Windows 线程同步是指多个线程一同访问共享资源时,为了避免资源的并发访问导致数据的不一致或程序崩溃等问题,需要对线程的访问进行协同和控制,以保证程序的正确性和稳定性。Windows提供了多种线程同步机制,以适应不同的并发编程场景。主要包括以下几种:
- 事件(Event):用于不同线程间的信号通知。包括单次通知事件和重复通知事件两种类型。
- 互斥量(Mutex):用于控制对共享资源的访问,具有独占性,可避免线程之间对共享资源的非法访问。
- 临界区(CriticalSection):和互斥量类似,也用于控制对共享资源的访问,但是是进程内部的,因此比较适用于同一进程中的线程同步控制。
- 信号量(Semaphore):用于基于计数器机制,控制并发资源的访问数量。
- 互锁变量(Interlocked Variable):用于对变量的并发修改操作控制,可提供一定程度的原子性操作保证。
以上同步机制各有优缺点和适用场景,开发者应根据具体应用场景进行选择和使用。在线程同步的实现过程中,需要注意竞争条件和死锁的处理,以确保程序中的线程能协同工作,共享资源能够正确访问和修改。线程同步是并发编程中的重要基础,对于开发高效、稳定的并发应用至关重要。
9.2.1 CreateEvent
CreateEvent 是Windows API提供的用于创建事件对象的函数之一,该函数用于创建一个事件对象,并返回一个表示该事件对象的句柄。可以通过SetEvent函数将该事件对象设置为有信号状态,通过ResetEevent函数将该事件对象设置为无信号状态。当使用WaitForSingleObject或者WaitForMultipleObjects函数等待事件对象时,会阻塞线程直到事件状态被置位。对于手动重置事件,需要调用ResetEvent函数手动将事件状态置位。
CreateEvent 函数常用于线程同步和进程间通信,在不同线程或者进程之间通知事件状态的改变。例如,某个线程完成了一项任务,需要通知其它等待该任务完成的线程;或者某个进程需要和另一个进程进行协调,需要通知其它进程某个事件的发生等等。
CreateEvent 函数的函数原型如下:- HANDLE CreateEvent(
- LPSECURITY_ATTRIBUTES lpEventAttributes,
- BOOL bManualReset,
- BOOL bInitialState,
- LPCTSTR lpName
- );
复制代码 参数说明:
- lpEventAttributes:指向SECURITY_ATTRIBUTES结构体的指针,指定事件对象的安全描述符和访问权限。通常设为NULL,表示使用默认值。
- bManualReset:指定事件对象的类型,TRUE表示创建的是手动重置事件,FALSE表示创建的是自动重置事件。
- bInitialState:指定事件对象的初始状态,TRUE表示将事件对象设为有信号状态,FALSE表示将事件对象设为无信号状态。
- lpName:指定事件对象的名称,可以为NULL。
CreateEvent 是实现线程同步和进程通信的重要手段之一,应用广泛且易用。在第一章中我们创建的多线程环境可能会出现线程同步的问题,此时使用Event事件机制即可很好的解决,首先在初始化时通过CreateEvent将事件设置为False状态,进入ThreadFunction线程时再次通过SetEvent释放,以此即可实现线程同步顺序执行的目的。- #include <stdio.h>
- #include <process.h>
- #include <windows.h>
- // 全局资源
- long g_nNum = 0;
- // 子线程个数
- const int THREAD_NUM = 10;
- CRITICAL_SECTION g_csThreadCode;
- HANDLE g_hThreadEvent;
- unsigned int __stdcall ThreadFunction(void *ptr)
- {
- int nThreadNum = *(int *)ptr;
- // 线程函数中触发事件
- SetEvent(g_hThreadEvent);
- // 进入线程锁
- EnterCriticalSection(&g_csThreadCode);
- g_nNum++;
- printf("线程编号: %d --> 全局资源值: %d --> 子线程ID: %d \n", nThreadNum, g_nNum, GetCurrentThreadId());
- // 离开线程锁
- LeaveCriticalSection(&g_csThreadCode);
- return 0;
- }
- int main(int argc,char * argv[])
- {
- unsigned int ThreadCount = 0;
- HANDLE handle[THREAD_NUM];
- // 初始化自动将事件设置为False
- g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
- InitializeCriticalSection(&g_csThreadCode);
- for (int each = 0; each < THREAD_NUM; each++)
- {
- handle[each] = (HANDLE)_beginthreadex(NULL, 0, ThreadFunction, &each, 0, &ThreadCount);
- // 等待线程事件被触发
- WaitForSingleObject(g_hThreadEvent, INFINITE);
- }
- WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
- // 销毁事件
- CloseHandle(g_hThreadEvent);
- DeleteCriticalSection(&g_csThreadCode);
- system("pause");
- return 0;
- }
复制代码 当然了事件对象同样可以实现更为复杂的同步机制,在如下我们在创建对象时,可以设置non-signaled状态运行的auto-reset模式,当我们设置好我们需要的参数时,可以直接使用SetEvent(hEvent)设置事件状态,则会自动执行线程函数。
要创建一个manual-reset模式并且初始状态为not-signaled的事件对象,需要按照以下步骤:
首先定义一个SECURITY_ATTRIBUTES结构体变量,设置其中的参数为NULL表示使用默认安全描述符,例如。- SECURITY_ATTRIBUTES sa = {0};
- sa.nLength = sizeof(sa);
- sa.lpSecurityDescriptor = NULL;
- sa.bInheritHandle = FALSE;
复制代码 接着调用CreateEvent函数创建事件对象,将bManualReset和bInitialState参数设置为FALSE,表示创建manual-reset模式的事件对象并初始状态为not-signaled。例如:- HANDLE hEvent = CreateEvent(
- &sa, // 安全属性
- TRUE, // Manual-reset模式
- FALSE, // Not-signaled 初始状态
- NULL // 事件对象名称
- );
复制代码 这样,我们就创建了一个名为hEvent的manual-reset模式的事件对象,初始状态为not-signaled。可以通过SetEvent函数将事件对象设置为signaled状态,通过ResetEvent函数将事件对象设置为non-signaled状态,也可以通过WaitForSingleObject或者WaitForMultipleObjects函数等待事件对象的状态变化。- #include <windows.h>
- #include <stdio.h>
- #include <process.h>
- #define STR_LEN 100
- // 存储全局字符串
- static char str[STR_LEN];
- // 设置事件句柄
- static HANDLE hEvent;
- // 统计字符串中是否存在A
- unsigned WINAPI NumberOfA(void *arg)
- {
- int cnt = 0;
- // 等待线程对象事件
- WaitForSingleObject(hEvent, INFINITE);
- for (int i = 0; str[i] != 0; i++)
- {
- if (str[i] == 'A')
- cnt++;
- }
- printf("Num of A: %d \n", cnt);
- return 0;
- }
- // 统计字符串总长度
- unsigned WINAPI NumberOfOthers(void *arg)
- {
- int cnt = 0;
- // 等待线程对象事件
- WaitForSingleObject(hEvent, INFINITE);
- for (int i = 0; str[i] != 0; i++)
- {
- if (str[i] != 'A')
- cnt++;
- }
- printf("Num of others: %d \n", cnt - 1);
- return 0;
- }
- int main(int argc, char *argv[])
- {
- HANDLE hThread1, hThread2;
- // 以non-signaled创建manual-reset模式的事件对象
- // 该对象创建后不会被立即执行,只有我们设置状态为Signaled时才会继续
- hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
- hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);
- hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);
- fputs("Input string: ", stdout);
- fgets(str, STR_LEN, stdin);
- // 字符串读入完毕后,将事件句柄改为signaled状态
- SetEvent(hEvent);
- WaitForSingleObject(hThread1, INFINITE);
- WaitForSingleObject(hThread2, INFINITE);
- // non-signaled 如果不更改,对象继续停留在signaled
- ResetEvent(hEvent);
- CloseHandle(hEvent);
- system("pause");
- return 0;
- }
复制代码 9.2.2 CreateSemaphore
CreateSemaphore 是Windows API提供的用于创建信号量的函数之一,用于控制多个线程之间对共享资源的访问数量。该函数常用于创建一个计数信号量对象,并返回一个表示该信号量对象的句柄。可以通过ReleaseSemaphore函数将该信号量对象的计数加1,通过WaitForSingleObject或者WaitForMultipleObjects函数等待信号量对象的计数变成正数以后再将其减1,以实现对共享资源访问数量的控制。
CreateSemaphore 函数常用于实现生产者消费者模型、线程池、任务队列等并发编程场景,用于限制访问共享资源的线程数量。信号量机制更多时候被用于限制资源的数量而不是限制线程的数量,但也可以用来实现一些线程同步场景。
该函数的函数原型如下:- HANDLE CreateSemaphore(
- LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
- LONG lInitialCount,
- LONG lMaximumCount,
- LPCTSTR lpName
- );
复制代码 参数说明:
- lpSemaphoreAttributes:指向SECURITY_ATTRIBUTES结构体的指针,指定信号量对象的安全描述符和访问权限。通常设为NULL,表示使用默认值。
- lInitialCount:指定信号量对象的初始计数,表示可以同时访问共享资源的线程数量。
- lMaximumCount:指定信号量对象的最大计数,表示信号量对象的计数上限。
- lpName:指定信号量对象的名称,可以为NULL。
总的来说,CreateSemaphore 是实现线程同步和进程通信,控制对共享资源的访问数量的重要手段之一,如下一段演示代码片段则通过此方法解决了线程通过问题,首先调用CreateSemaphore初始化时将信号量设置一个最大值,每次进入线程函数内部时,则ReleaseSemaphore信号自动加1,如果大于指定的数值则WaitForSingleObject等待释放信号.- #include <stdio.h>
- #include <process.h>
- #include <windows.h>
- // 全局资源
- long g_nNum = 0;
- // 子线程个数
- const int THREAD_NUM = 10;
- CRITICAL_SECTION g_csThreadCode;
- HANDLE g_hThreadParameter;
- unsigned int __stdcall ThreadFunction(void *ptr)
- {
- int nThreadNum = *(int *)ptr;
- // 信号量++
- ReleaseSemaphore(g_hThreadParameter, 1, NULL);
- // 进入线程锁
- EnterCriticalSection(&g_csThreadCode);
- g_nNum++;
- printf("线程编号: %d --> 全局资源值: %d --> 子线程ID: %d \n", nThreadNum, g_nNum, GetCurrentThreadId());
- // 离开线程锁
- LeaveCriticalSection(&g_csThreadCode);
- return 0;
- }
- int main(int argc,char * argv[])
- {
- unsigned int ThreadCount = 0;
- HANDLE handle[THREAD_NUM];
- // 初始化信号量当前0个资源,最大允许1个同时访问
- g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL);
- InitializeCriticalSection(&g_csThreadCode);
- for (int each = 0; each < THREAD_NUM; each++)
- {
- handle[each] = (HANDLE)_beginthreadex(NULL, 0, ThreadFunction, &each, 0, &ThreadCount);
- // 等待信号量>0
- WaitForSingleObject(g_hThreadParameter, INFINITE);
- }
- // 关闭信号
- CloseHandle(g_hThreadParameter);
- // 等待所有进程结束
- WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
- DeleteCriticalSection(&g_csThreadCode);
- system("pause");
- return 0;
- }
复制代码 如下所示代码片段,是一个应用了两个线程的案例,初始化信号为0,利用信号量值为0时进入non-signaled状态,大于0时进入signaled状态的特性即可实现线程同步。
执行WaitForSingleObject(semTwo, INFINITE);会让线程函数进入类似挂起的状态,当接到ReleaseSemaphore(semOne, 1, NULL);才会恢复执行。- #include <windows.h>
- #include <stdio.h>
- static HANDLE semOne,semTwo;
- static int num;
- // 线程函数A用于接收参书
- DWORD WINAPI ReadNumber(LPVOID lpParamter)
- {
- int i;
- for (i = 0; i < 5; i++)
- {
- fputs("Input Number: ", stdout);
- // 临界区的开始 signaled状态
- WaitForSingleObject(semTwo, INFINITE);
-
- scanf("%d", &num);
- // 临界区的结束 non-signaled状态
- ReleaseSemaphore(semOne, 1, NULL);
- }
- return 0;
- }
- // 线程函数B: 用户接受参数后完成计算
- DWORD WINAPI Check(LPVOID lpParamter)
- {
- int sum = 0, i;
- for (i = 0; i < 5; i++)
- {
- // 临界区的开始 non-signaled状态
- WaitForSingleObject(semOne, INFINITE);
- sum += num;
- // 临界区的结束 signaled状态
- ReleaseSemaphore(semTwo, 1, NULL);
- }
- printf("The Number IS: %d \n", sum);
- return 0;
- }
- int main(int argc, char *argv[])
- {
- HANDLE hThread1, hThread2;
- // 创建信号量对象,设置为0进入non-signaled状态
- semOne = CreateSemaphore(NULL, 0, 1, NULL);
- // 创建信号量对象,设置为1进入signaled状态
- semTwo = CreateSemaphore(NULL, 1, 1, NULL);
- hThread1 = CreateThread(NULL, 0, ReadNumber, NULL, 0, NULL);
- hThread2 = CreateThread(NULL, 0, Check, NULL, 0, NULL);
- // 关闭临界区
- WaitForSingleObject(hThread1, INFINITE);
- WaitForSingleObject(hThread2, INFINITE);
- CloseHandle(semOne);
- CloseHandle(semTwo);
- system("pause");
- return 0;
- }
复制代码 9.2.3 CreateMutex
CreateMutex 是Windows API提供的用于创建互斥体对象的函数之一,该函数用于创建一个互斥体对象,并返回一个表示该互斥体对象的句柄。可以通过WaitForSingleObject或者WaitForMultipleObjects函数等待互斥体对象,以确保只有一个线程能够访问共享资源,其他线程需要等待该线程释放互斥体对象后才能继续访问。当需要释放互斥体对象时,可以调用ReleaseMutex函数将其释放。
CreateMutex 函数常用于对共享资源的访问控制,避免多个线程同时访问导致数据不一致的问题。有时候,互斥体也被用于跨进程同步访问共享资源。
该函数的函数原型如下:- HANDLE CreateMutex(
- LPSECURITY_ATTRIBUTES lpMutexAttributes,
- BOOL bInitialOwner,
- LPCTSTR lpName
- );
复制代码 参数说明:
- lpMutexAttributes:指向SECURITY_ATTRIBUTES结构体的指针,指定互斥体对象的安全描述符和访问权限。通常设为NULL,表示使用默认值。
- bInitialOwner:指定互斥体的初始状态,TRUE表示将互斥体设置为有所有权的状态,FALSE表示将互斥体设置为没有所有权的状态。
- lpName:指定互斥体的名称,可以为NULL。
该函数是实现线程同步和进程通信,控制对共享资源的访问的重要手段之一,应用广泛且易用。
如下案例所示,使用互斥锁可以实现单位时间内,只允许一个线程拥有对共享资源的独占权限,从而实现了互不冲突的线程同步。
[code]#include #include using namespace std;// 创建互斥锁HANDLE hMutex = NULL;// 线程函数DWORD WINAPI Func(LPVOID lpParamter){ for (int x = 0; x < 10; x++) { // 请求获得一个互斥锁 WaitForSingleObject(hMutex, INFINITE); cout |