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++线程同步方式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!