线程同步的方法:使用临界区对象,互斥函数,事件,信号量,互斥
临界区对象
- 临界区对象是定义在数据段中的一个 CRITICAL_SECTION 结构
- void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection );
- // 指向数据段中定义的CRITICAL_SECTION 结构
- 线程访问临界区中数据的时候,必须首先调用EnterCriticalSection 函数,申请进入临界区(又叫关键代码段),如果有另一个线程在临界区,EnterCriticalSection 函数会一直等待下去,直到其他线程离开临界区才返回
- void EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
- 操作完成的时候,还要将临界区交还给 Windows,这个工作由LeaveCriticalSection 函数来完成。
- void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
- 当程序不再使用临界区对象的时候,必须使用DeleteCriticalSection 函数将它删除。
- void DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
- 举例:两个线程同时使两个全局变量自增。
#include <stdio.h> #include <windows.h> #include <process.h> BOOL g_bContinue = TRUE; int g_nCount1 = 0; int g_nCount2 = 0; CRITICAL_SECTION g_cs; // 对存在同步问题的代码段使用临界区对象 UINT __stdcall ThreadFunc(LPVOID); int main(int argc, char* argv[]) { UINT uId; HANDLE h[2]; // 初始化临界区对象 ::InitializeCriticalSection(&g_cs); h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId); h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId); // 等待1秒后通知两个计数线程结束,关闭句柄 Sleep(1000); g_bContinue = FALSE; ::WaitForMultipleObjects(2, h, TRUE, INFINITE); ::CloseHandle(h[0]); ::CloseHandle(h[1]); // 删除临界区对象 ::DeleteCriticalSection(&g_cs); printf("g_nCount1 = %d \n", g_nCount1); printf("g_nCount2 = %d \n", g_nCount2); return 0; } UINT __stdcall ThreadFunc(LPVOID) { while(g_bContinue) { ::EnterCriticalSection(&g_cs); g_nCount1++; g_nCount2++; ::LeaveCriticalSection(&g_cs); } return 0; }
- PS: __stdcall 是新标准C/C++函数的调用方法。从底层上说,使用这种调用方法参数的进栈顺序和标准C 调用(_cdecl 方法)是一样的,都是从右到左,但是__stdcall 采用自动清栈的方式,而_cdecl 采用的是手工清栈方式。Windows 规定,凡是由它来负责调用的函数都必须定义为__stdcall 类型。注意,如果没有显式说明的话,函数的调用方法是_cdecl。
- 临界区对象能够很好地保护共享数据,但是它不能够用于进程之间资源的锁定,因为它不是内核对象。如果要在进程间维持线程的同步可以使用事件内核对象。
互斥函数
- 互锁函数为同步访问多线程共享变量提供了一个简单的机制。如果变量在共享内存,不同进程的线程也可以使用此机制。用于互锁的函数有 InterlockedIncrement、InterlockedDecrement、InterlockedExchangeAdd、InterlockedExchangePointer 等
- l InterlockedIncrement 函数递增(加1)指定的32 位变量。这个函数可以阻止其他线程同时使用此变量,函数原型如
- LONGInterlockedDecrement( LONG volatile* Addend); // 指向要递减的变量
- l InterlockedDecrement 函数同步递减(减1)指定的32 位变量,原型如下。
- LONGInterlockedIncrement( LONG volatile* Addend); // 指向要递增的变量
- 例如
UINT __stdcall ThreadFunc(LPVOID) // 03InterlockDemo 工程下 { while(g_bContinue) { ::InterlockedIncrement((long*)&g_nCount1); ::InterlockedIncrement((long*)&g_nCount2); } return 0; }
- l InterlockedIncrement 函数递增(加1)指定的32 位变量。这个函数可以阻止其他线程同时使用此变量,函数原型如
事件内核对象
- 事件内核对象用于主线程和工作线程的通信。事件对象(event)是一种抽象的对象,它也有未受信(nonsignaled)和受信(signaled)两种状态,编程人员也可以使用WaitForSingleObject 函数等待其变成受信状态。
- 事件对象包含3 个成员:nUsageCount (使用计数)、bManualReset(是否人工重置)和bSignaled(是否受信)。
- 成员 nUsageCount 记录了当前的使用计数,初始状态为1,当使用计数为0 的时候,Windows 就会销毁此内核对象占用的资源;
- 成员 bManualReset 指定在一个事件内核对象上等待的函数返回之后,Windows 是否重置这个对象为未受信状态;
- 成员 bSignaled 指定当前事件内核对象是否受信。下面要介绍的操作事件内核对象的函数会影响这些成员的值。
- 基本函数
- HANDLE CreateEvent(
- LPSECURITY_ATTRIBUTES lpEventAttributes, // 用来定义事件对象的安全属性
- BOOL bManualReset, // 指定是否需要手动重置事件对象为未受信状态。
- BOOL bInitialState, // 指定事件对象创建时的初始状态
- LPCWSTR lpName); // 事件对象的名称
- 参数 bManualReset 对应着内核对象中的 bManualReset 成员。
- 自动重置(auto-reset)和人工重置(manual-reset)是事件内核对象两种不同的类型。
- 当一个人工重置的事件对象受信以后,所有等待在这个事件上的线程都会变为可调度状态;
- 可是当一个自动重置的事件对象受信以后,Windows 仅允许一个等待在该事件上的线程变成可调度状态,然后就自动重置此事件对象为未受信状态。
- bInitialState 参数对应着 bSignaled 成员。将它设为TRUE,则表示事件对象创建时的初始化状态为受信(bSignaled = TRUE);设为FALSE 时,状态为未受信(bSignaled = FALSE)。
- lpName 参数用来指定事件对象的名称。
- HANDLE OpenEvent (
- DWORD dwDesiredAccess, // 指定想要的访问权限
- BOOL bInheritHandle, // 指定返回句柄是否可被继承
- LPCWSTR lpName); // 要打开的事件对象的名称
- 不使用此内核对象的时候,应该调用 CloseHandle 函数释放它占用的资源。
- 事件对象被建立后,程序可以通过SetEvent 和ResetEvent 函数来设置它的状态。
- BOOL SetEvent( HANDLE hEvent ); // 将事件状态设为 “受信(sigaled)”;
- BOOL ResetEvent(HANDLE hEvent ); // 将事件状态设为 “未受信(nonsigaled)”;
- 当在这样的事件对象上等待的函数(比如,WaitForSingleObject 函数)返回时,Windows 会自动重置事件
对象为未受信状态。
- 举例:主线程通过将事件状态设为“受信”来通知子线程开始工作。
#include <stdio.h> #include <windows.h> #include <process.h> HANDLE g_hEvent; UINT __stdcall ChildFunc(LPVOID); int main(int argc, char* argv[]) { HANDLE hChildThread; UINT uId; // 创建一个自动重置的(auto-reset events),未受信的(nonsignaled)事件内核对象 g_hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL); hChildThread = (HANDLE)::_beginthreadex(NULL, 0, ChildFunc, NULL, 0, &uId); // 通知子线程开始工作 printf("Please input a char to tell the Child Thread to work: \n"); getchar(); ::SetEvent(g_hEvent); // 等待子线程完成工作,释放资源 ::WaitForSingleObject(hChildThread, INFINITE); printf("All the work has been finished. \n"); ::CloseHandle(hChildThread); ::CloseHandle(g_hEvent); return 0; } UINT __stdcall ChildFunc(LPVOID) { ::WaitForSingleObject(g_hEvent, INFINITE); printf(" Child thread is working...... \n"); ::Sleep(5*1000); // 暂停5秒,模拟真正的工作 return 0; }
信号量内核对象
- 信号量 (Semaphore) 内核对象对线程的同步方式与前面几种方法不同,它允许多个线程在同一时刻访问同一资源,但是需要限制同一时刻访问的最大线程数目。
- 主要用到的函数:CreateSemaphore、OpenSemaphore、ReleaseSemaphore、WaitForSingleObject、WaitForMultipleObject。
- CreateSemaphore(
- LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性指针
- LONG lInitialCount, // 初始计数
- LONG lMaximumCount, // 最大计数,不超过4294967295
- LPCTSTR lpName) // 对象名指针
- HANDLE OpenSemaphore(
- DWORD dwDesiredAccess, // 访问标志
- BOOL bInheritHandle,// 继承标志
- LPCTSTR lpName)// 信号量名
- BOOL ReleaseSemaphore(
- HANDLE hSemaphore, // 信号量句柄
- LONG lReleaseCount,// 计数递增数量
- LPLONG lpPreviousCount)// 先前计数
- WaitForSingleObject、WaitForMultipleObject 主要是用在视图进入共享资源的线程函数入口处,主要用来判断信号量的当前可用资源计数是否允许本线程的进入。只有在当前可用资源计数值大于0时,被监视的信号量内核对象才会得到通知。
- CreateSemaphore(
互斥内核对象
- 互斥内核对象能够保证多个线程对同一共享资源的互斥访问。
- 互斥内核对象的主要函数:CreateMutex, OpenMutex, ReleaseMutex, WaitforSingleOject, WaitForMultipleObjects等。在使用互斥对象前,首先言通过 CreateMutex 或 OpenMutex 创建或打开一个互斥对象。
- HANDLE CreateMutex(
- LPSECURITY_ATTRIBUTES lpMutexAttributes,// 安全属性指针
- BOOL bInitialOwner,// 初始拥有者
- LPCTSTR lpName)// 互斥对象名
- bInitialOwner 一般将它初始为 false,表示无线程占有
- bInitialOwner 一般将它初始为 false,表示无线程占有
- HANDLE OpenMutex(
- DWORD dwDesiredAccess// 访问标志
- BOOL bInheritHandle,// 继承标志
- LPCTSTR lpName)// 互斥对象名
- BOOL ReleaseMutex(HANDLE hMutex);
- 当前访问资源的线程不再访问次资源而离开时,释放互斥对象
- HANDLE CreateMutex(