C++实现线程同步的四种方式总结

目录
  • 内核态
    • 互斥变量
    • 事件对象
    • 资源信号量
  • 用户态
    • 关键代码

内核态

互斥变量

互斥对象包含一个使用数量,一个线程ID和一个计数器。其中线程ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。

创建互斥对象:调用函数CreateMutex。调用成功,该函数返回所创建的互斥对象的句柄。

请求互斥对象所有权:调用函数WaitForSingleObject函数。线程必须主动请求共享对象的所有权才能获得所有权。

释放指定互斥对象的所有权:调用ReleaseMutex函数。线程访问共享资源结束后,线程要主动释放对互斥对象的所有权,使该对象处于已通知状态。

创建互斥对象函数

HANDLE
WINAPI
CreateMutexW(
    _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,   //指向安全属性
    _In_ BOOL bInitialOwner,   //初始化互斥对象的所有者  TRUE 立即拥有互斥体
    _In_opt_ LPCWSTR lpName    //指向互斥对象名的指针  L“Bingo”
);
  • 第一个参数表示安全属性,这是每一个创建内核对象都会有的参数,NULL表示默认安全属性
  • 第二个参数表示互斥对象所有者,TRUE立即拥有互斥体
  • 第三个参数表示指向互斥对象的指针

代码示例

下面这段程序声明了一个全局整型变量,并初始化为0。一个线程函数对这个变量进行+1操作,执行50000次;另一个线程函数对这个变量-1操作,执行50000次。两个线程函数各创建25个。因为我们使用了互斥变量,50个线程会按照一定顺序对这变量操作,因此最后结果为0。

#include <stdio.h>
#include <windows.h>
#include <process.h>

#define NUM_THREAD    50
unsigned WINAPI threadInc(void* arg);
unsigned WINAPI threadDes(void* arg);
long long num = 0;
HANDLE hMutex;

int main() {
    //内核对象数组
    HANDLE tHandles[NUM_THREAD];
    int i;
    //创建互斥信号量
    hMutex = CreateMutex(0, FALSE, NULL);
    printf("sizeof long long: %d \n", sizeof(long long));
    for (i = 0; i < NUM_THREAD; i++) {
        if (i % 2)
            tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
        else
            tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
    }

    WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
    //关闭互斥对象
    CloseHandle(hMutex);
    printf("result: %lld \n", num);
    return 0;
}

unsigned WINAPI threadInc(void* arg){
    int i;
    //请求使用
    WaitForSingleObject(hMutex, INFINITE);
    for (i = 0; i < 500000; i++)
        num += 1;
    //释放
    ReleaseMutex(hMutex);
    return 0;
}
unsigned WINAPI threadDes(void* arg){
    int i;
    //请求
    WaitForSingleObject(hMutex, INFINITE);
    for (i = 0; i < 500000; i++)
        num -= 1;
    //释放
    ReleaseMutex(hMutex);
    return 0;
}

事件对象

事件对象也属于内核对象,它包含以下三个成员:

  • 使用计数;
  • 用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值;
  • 用于指明该事件处于已通知状态还是未通知状态的布尔值。

事件对象有两种类型:人工重置的事件对象和自动重置的事件对象。这两种事件对象的区别在于当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;而当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。

1.创建事件对象

调用CreateEvent函数创建或打开一个命名的或匿名的事件对象。

HANDLE CreateEvent(   
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性   
BOOL bManualReset,   // 复位方式  TRUE 必须用ResetEvent手动复原  FALSE 自动还原为无信号状态
BOOL bInitialState,   // 初始状态   TRUE 初始状态为有信号状态  FALSE 无信号状态
LPCTSTR lpName     //对象名称  NULL  无名的事件对象 
);
  • 第一个参数表示安全属性,这是创建内核对象函数都有的一个参数,NULL表示默认安全属性
  • 第二个参数表示复位方式,如果是TRUE,则必须手动调用ResetEvent函数复位,FALSE则表示自动还原
  • 第三个参数表示初始状态,TRUE表示初始为有信号状态,FALSE为无信号
  • 第四个参数表示对象名称,NULL表示无名的事件对象

2. 设置事件对象状态

调用SetEvent函数把指定的事件对象设置为有信号状态。

3. 重置事件对象状态

调用ResetEvent函数把指定的事件对象设置为无信号状态。

4. 请求事件对象

线程通过调用WaitForSingleObject函数请求事件对象。

代码示例

下面这段程序是一段火车售票:线程A和B会不停的购票直到票数小于0,执行完毕。在判断票数前会先申请事件对象,购票结束或者票数小于0时则会释放事件对象(事件对象置位有信号)。因为我们使用了事件对象。两个线程会按某一顺序购票,直到票数小于0。

#include<iostream>
#include<Windows.h>
#include<process.h>
using namespace std;

//火车站卖票
int iTickets = 100;//总票数
HANDLE g_hEvent;

unsigned WINAPI SellTicketA(void* lpParam) {

    while (true) {
        WaitForSingleObject(g_hEvent, INFINITE);
        if (iTickets > 0) {
            Sleep(1);
            printf("A买了一张票,剩余%d\n", iTickets--);
        }
        else {
            SetEvent(g_hEvent);
            break;
        }
        SetEvent(g_hEvent);
    }
    return 0;
}

unsigned WINAPI SellTicketB(void* lpParam) {
    while (true) {
        WaitForSingleObject(g_hEvent, INFINITE);
        if (iTickets > 0) {
            Sleep(1);
            printf("B买了一张票,剩余%d\n", iTickets--);
        }
        else {
            SetEvent(g_hEvent);
            break;
        }
        SetEvent(g_hEvent);
    }
    return 0;
}

int main() {

    HANDLE hThreadA, hThreadB;
    hThreadA = (HANDLE)_beginthreadex(NULL, 0, SellTicketA, NULL, 0, NULL);
    hThreadB = (HANDLE)_beginthreadex(NULL, 0, SellTicketB, NULL, 0, NULL);

    CloseHandle(hThreadA);
    CloseHandle(hThreadB); 

    g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    SetEvent(g_hEvent);
    Sleep(4000);
    CloseHandle(g_hEvent);
    system("pause");
    return 0;
}

资源信号量

信号量(semaphore)是操作系统用来解决并发中的互斥和同步问题的一种方法。与互斥量不同的地方是,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。

创建信号量函数

HANDLE    WINAPI
CreateSemaphoreW(
    _In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,  // Null 安全属性
    _In_ LONG lInitialCount,  //初始化时,共有多少个资源是可以用的。 0:未触发状//态(无信号状态),表示没有可用资源
    _In_ LONG lMaximumCount,  //能够处理的最大的资源数量   3
    _In_opt_ LPCWSTR lpName   //NULL 信号量的名称
);
  • 第一个参数表示安全属性,这是创建内核对象函数都会有的参数,NULL表示默认安全属性
  • 第二个参数表示初始时有多少个资源可用,0表示无任何资源(未触发状态)
  • 第三个参数表示最大资源数
  • 第四个参数表示信号量的名称,NULL表示无名称的信号量对象

增加/释放信号量

ReleaseSemaphore(
    _In_ HANDLE hSemaphore,   //信号量的句柄
    _In_ LONG lReleaseCount,   //将lReleaseCount值加到信号量的当前资源计数上面 0-> 1
    _Out_opt_ LPLONG lpPreviousCount  //当前资源计数的原始值
);
  • 第一个参数表示信号量句柄,也就是调用创建信号量函数时返回的句柄
  • 第二个参数表示释放的信号量个数,该值必须大于0,但不能大于信号量的最大计数
  • 第三个参数表示指向要接收信号量的上一个计数的变量的指针。如果不需要上一个计数, 则此参数可以为NULL 。

关闭句柄

CloseHandle(
    _In_ _Post_ptr_invalid_ HANDLE hObject
);

代码示例

下面这段程序创建了两个信号资源,其最大资源都为1;一个初始资源为0,另一个初始资源为1。线程中的for循环每执行一次会将另一个要申请的信号资源的可用资源数+1。因此程序的执行结果为两个线程中的for循环交替执行。

#include<iostream>
#include<Windows.h>
#include<process.h>
using namespace std;

static HANDLE semOne;
static HANDLE semTwo;
static int num;

/*
* 信号资源semOne初始为0,最大1个资源可用
* 信号资源semTwo初始为1,最大1个资源可用
*/

unsigned WINAPI Read(void* arg) {
    int i;
    for (i = 0; i < 5; i++) {
        fputs("Input num:\n", stdout);
        printf("begin read\n");
        WaitForSingleObject(semTwo, INFINITE);
        printf("beginning read\n");
        scanf("%d", &num);
        ReleaseSemaphore(semOne, 1, NULL);
    }
    return 0;
}

unsigned WINAPI Accu(void* arg) {
    int sum = 0, i;
    for (i = 0; i < 5; ++i) {
        printf("begin Accu\n");
        WaitForSingleObject(semOne, INFINITE);
        printf("beginning Accu\n");
        sum += num;
        printf("sum=%d\n", sum);
        ReleaseSemaphore(semTwo, 1, NULL);
    }
    return 0;
}

int main() {
    HANDLE hThread1, hThread2;
    semOne = CreateSemaphore(NULL, 0, 1, NULL);//初始值没有可用资源
    semTwo = CreateSemaphore(NULL, 1, 1, NULL);//初始值有一个可用资源

    hThread1 = (HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);
    hThread2 = (HANDLE)_beginthreadex(NULL, 0, Accu, NULL, 0, NULL);
    WaitForSingleObject(hThread1, INFINITE);
    WaitForSingleObject(hThread2, INFINITE);

    CloseHandle(semOne);
    CloseHandle(semTwo);
    system("pause");
    return 0;
}

用户态

关键代码

关键代码段,也称为临界区,工作在用户方式下。它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。通常把多线程中访问同一种资源的那部分代码当做关键代码段。

1.初始化关键代码段

调用InitializeCriticalSection函数初始化一个关键代码段

InitialzieCriticalSection(
    _Out_ LPRRITICAL_SECTION lpCriticalSection
);

该函数只有一个指向CRITICAL_SECTION结构体的指针。在调用InitializeCriticalSection函数之前,首先需要构造一个CRITICAL_SCTION结构体类型的对象,然后将该对象的地址传递给InitializeCriticalSection函数。

2进入关键代码

VOID
WINAPI
EnterCriticalSection(
    _Inout_ LPCRITICAL_SECTION lpCriticalSection
);

调用EnterCriticalSection函数,以获得指定的临界区对象的所有权,该函数等待指定的临界区对象的所有权,如果该所有权赋予了调用线程,则该函数就返回;否则该函数会一直等待,从而导致线程等待。

3.退出关键代码段

VOID
WINAPI
LeaveCriticalSection(
    _Inout_ LPCRITICAL_SECTION lpCriticalSection
);

线程使用完临界区所保护的资源之后,需要调用LeaveCriticalSection函数,释放指定的临界区对象的所有权。之后,其他想要获得该临界区对象所有权的线程就可以获得该所有权,从而进入关键代码段,访问保护的资源。

4.删除临界区

WINBASEAPI
VOID
WINAPI
DeleteCriticalSection(
    _Inout_ LPCRITICAL_SECTION lpCriticalSection
);

当临界区不再需要时,可以调用DeleteCriticalSection函数释放该对象,该函数将释放一个没有被任何线程所拥有的临界区对象的所有资源。

程序实例:

下面这段程序同样也是火车售票,其工作逻辑与上面的事件对象基本吻合。

#include<iostream>
#include<Windows.h>
#include<process.h>
using namespace std;

int iTickets = 100;
CRITICAL_SECTION g_cs;

//A窗口
DWORD WINAPI SellTicketA(void* lpParam) {
    while (1) {
        EnterCriticalSection(&g_cs);//进入临界区
        if (iTickets > 0) {
            Sleep(1);
            iTickets--;
            printf("A买了一张票,剩余票数为:%d\n", iTickets);
            LeaveCriticalSection(&g_cs);
        }
        else {
            LeaveCriticalSection(&g_cs);
            break;
        }
    }
    return 0;
}

//B窗口
DWORD WINAPI SellTicketB(void* lpParam) {
    while (1) {
        EnterCriticalSection(&g_cs);
        if (iTickets > 0) {
            Sleep(1);
            iTickets--;
            printf("B买了一张票,剩余票数为:%d\n", iTickets);
            LeaveCriticalSection(&g_cs);
        }
        else {
            LeaveCriticalSection(&g_cs);
            break;
        }
    }
    return 0;
}

int main() {
    HANDLE hThreadA, hThreadB;
    hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL);
    hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL);

    CloseHandle(hThreadA);
    CloseHandle(hThreadB);

    InitializeCriticalSection(&g_cs);//初始化关键代码
    Sleep(1000);

    DeleteCriticalSection(&g_cs);
    system("pause");
    return 0;
}

到此这篇关于C++实现线程同步的四种方式总结的文章就介绍到这了,更多相关C++线程同步方式内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++ 线程(串行 并行 同步 异步)详解

    C++  线程(串行 并行 同步 异步)详解 看了很多关于这类的文章,一直没有总结.不总结的话就会一直糊里糊涂,以下描述都是自己理解的非官方语言,不一定严谨,可当作参考. 首先,进程可理解成一个可执行文件的执行过程.在ios app上的话我们可以理解为我们的app的.ipa文件执行过程也即app运行过程.杀掉app进程就杀掉了这个app在系统里运行所占的内存. 线程:线程是进程的最小单位.一个进程里至少有一个主线程.就是那个main thread.非常简单的app可能只需要一个主线程即UI线程.

  • C++线程同步实例分析

    本文实例分析了C++线程同步问题,分享给大家供大家参考.具体分析如下: 该实例设置全局变量g_bContinue,在主线程中设置全局变量g_bContinue,工作线程检测该全局变量,实现主线程控制工作线程的目的. 打印出的g_cnt1与g_cnt2的数值不同,是因为线程调试时时间片的切换. 具体代码如下: // countError.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include <Windows.h> DWORD

  • C++ 如何实现多线程与线程同步

    CreateThread 实现多线程: 先来创建一个简单的多线程实例,无参数传递版,运行实例会发现,主线程与子线程运行无规律. #include <windows.h> #include <iostream> using namespace std; DWORD WINAPI Func(LPVOID lpParamter) { for (int x = 0; x < 10; x++) { cout << "thread function" &l

  • C++使用CriticalSection实现线程同步实例

    本文实例讲述了C++使用CriticalSection实现线程同步的方法,在前文C++线程同步实例分析的基础上增加了四行代码,使用了四个函数: EnterCriticalSection ::DeleteCriticalSection ::EnterCriticalSection ::LeaveCriticalSection此时,打印出来的数字就相等了. 具体代码如下: #include "stdafx.h" #include <Windows.h> DWORD g_cnt1

  • C语言细致讲解线程同步的集中方式

    目录 互斥锁 条件变量 信号量 读写锁 互斥锁 使用互斥量完成对临界区的资源的加锁操作,使得同一时刻,对一个共享数据的使用只能又一个线程完成 例向屏幕上一次打印abcd四个字母 可以使用的是一个类似锁连的思想 a 加完解开后拿b锁依次类推 #define THRNUM 4 static pthread_mutex_t mut[4]; static int next(int n) { if(n + 1 == THRNUM) return 0; return n+1; } static void*

  • C++实现线程同步的四种方式总结

    目录 内核态 互斥变量 事件对象 资源信号量 用户态 关键代码 内核态 互斥变量 互斥对象包含一个使用数量,一个线程ID和一个计数器.其中线程ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数. 创建互斥对象:调用函数CreateMutex.调用成功,该函数返回所创建的互斥对象的句柄. 请求互斥对象所有权:调用函数WaitForSingleObject函数.线程必须主动请求共享对象的所有权才能获得所有权. 释放指定互斥对象的所有权:调用ReleaseMutex函

  • linux下实现web数据同步的四种方式(性能比较)

    实现web数据同步的四种方式 ======================================= 1.nfs实现web数据共享2.rsync +inotify实现web数据同步3.rsync+sersync更快更节约资源实现web数据同步4.unison+inotify实现web数据双向同步 ======================================= 一.nfs实现web数据共享 nfs能实现数据同步是通过NAS(网络附加存储),在服务器上共享一个文件,且服务器需

  • Java线程创建的四种方式总结

    多线程的创建,方式一:继承于Thread类 1.创建一个继承于Thread类的子类 2.重写Thread类的run()--->将此线程执行的操作声明在run()中 3.创建Thread类的子类的对象 4.通过此对象调用start(): start()方法的两个作用: A.启动当前线程 B.调用当前线程的run() 创建过程中的两个问题: 问题一:我们不能通过直接调用run()的方式启动线程 问题二:在启动一个线程,遍历偶数,不可以让已经start()的线程去执行,会报异常:正确的方式是重新创建一

  • 如何在Java中创建线程通信的四种方式你知道吗

    目录 1.1 创建线程 1.1.1 创建线程的四种方式 1.1.2 Thread类与Runnable接口的比较 1.1.3 Callable.Future与FutureTask 1.2 线程组和线程优先级 1.3 Java线程的状态及主要转化方法 1.4 Java线程间的通信 1.4.1 等待/通知机制 1.4.2 信号量 1.4.3 管道 总结 1.1 创建线程 1.1.1 创建线程的四种方式 [1]继承Thread类 [2]实现Runnable接口 [3]实现Callable,获取返回值 [

  • Python多线程实现同步的四种方式

    临界资源即那些一次只能被一个线程访问的资源,典型例子就是打印机,它一次只能被一个程序用来执行打印功能,因为不能多个线程同时操作,而访问这部分资源的代码通常称之为临界区. 锁机制 threading的Lock类,用该类的acquire函数进行加锁,用realease函数进行解锁 import threading import time class Num: def __init__(self): self.num = 0 self.lock = threading.Lock() def add(s

  • java 创建线程的四种方式

    1.继承Thread类方式 这种方式适用于执行特定任务,并且需要获取处理后的数据的场景. 举例:一个用于累加数组内数据的和的线程. public class AdditionThread extends Thread { private int sum = 0; private int[] nums; ​ public AdditionThread(int[] nums, String threadName) { super(threadName); this.nums = nums; } ​

  • java线程池的四种创建方式详细分析

    目录 前言 1. 线程池 2. 创建方式 前言 在讲述线程池的前提 先补充一下连接池的定义 连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用 可以看到其连接池的作用如下: 1. 线程池 线程池(英语:thread pool):一种线程使用模式.线程过多会带来调度开销,进而影响缓存局部性和整体性能.而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务.这避免了在处理短时间任务时创建与销毁线程的代价.线程池不仅能够保证内核的充分利用,还能防止过分调度 特点:

  • Spring Boot异步线程间数据传递的四种方式

    目录 Spring Boot 自定义线程池实现异步开发 1. 手动设置 2. 线程池设置TaskDecorator 3. InheritableThreadLocal 4. TransmittableThreadLocal TransmittableThreadLocal原理 总结 Spring Boot 自定义线程池实现异步开发 Spring Boot 自定义线程池实现异步开发相信看过的都了解,但是在实际开发中需要在父子线程之间传递一些数据,比如用户信息,链路信息等等 比如用户登录信息使用Th

  • 关于python线程池的四种实现方式

    目录 python 线程池的四种实现方式 线程简述 方式1 multiprocessing.dummy Pool() 方式2:multiprocessing.pool ThreadPool Threading() 方式3:主流ThreadPoolExecutor 方式4:threadpool python 线程池的四种实现方式 线程简述 一个程序运行起来后,一定有一个执行代码的东西,这个东西就是线程: 一般计算(CPU)密集型任务适合多进程,IO密集型任务适合多线程:一个进程可拥有多个并行的(c

  • C#线程同步的几种方法总结

    我们在编程的时候,有时会使用多线程来解决问题,比如你的程序需要在后台处理一大堆数据,但还要使用户界面处于可操作状态:或者你的程序需要访问一些外部资源如数据库或网络文件等.这些情况你都可以创建一个子线程去处理,然而,多线程不可避免地会带来一个问题,就是线程同步的问题.如果这个问题处理不好,我们就会得到一些非预期的结果. 在网上也看过一些关于线程同步的文章,其实线程同步有好几种方法,下面我就简单的做一下归纳. 一.volatile关键字 volatile是最简单的一种同步方法,当然简单是要付出代价的

随机推荐