第八章用户模式下的线程同步
1、原子访问函数:Interlocked系列函数
对LONG类型数进行原子加减
LONG __cdecl InterlockedExchangeAdd(
__inout LONG volatile* Addend,
__in LONG Value //要加的值
);
对LONGLONG类型的数进行加减
LONGLONG __cdecl InterlockedExchangeAdd64(
__inout LONGLONG volatile* Addend,
__in LONGLONG Value
);
加1操作:
LONG __cdecl InterlockedIncrement(
__inout LONG volatile* Addend
);
将一个值赋给另一个值的原子操作:函数返回目标的初始值
LONG __cdecl InterlockedExchange(//32位值替换
__inout LONG volatile* Target,
__in LONG Value
);
PVOID __cdecl InterlockedExchangePointer(//64位值替换
__inout PVOID volatile* Target,
__in PVOID Value
);
旋转锁:
// 全局变量来指示共享资源是否可用 BOOL g_fResourceInUse = FALSE; ... void Func1() { // 等待使用共享资源 while (InterlockedExchange (&g_fResourceInUse, TRUE) == TRUE) Sleep(0); // 使用共享资源. //... // 不再需要使用共享资源了. InterlockedExchange(&g_fResourceInUse, FALSE); }在单CPU机器上应避免使用旋转锁,旋转锁假定被保护的资源始终只会占用一小段时间。
2、关键段
关键段是一小段代码,它在执行之前需要独占对这一些共享资源的访问权,从而让这些代码以原子方式来对资源进行操控。int g_n_Sum = 0;//全局变量 CRITICAL_SECTION g_cs; InitializeCriticalSection(&g_cs);//使用前必须初始化 DWORD WINAPI FirstThread(PVOID pvParam){ EnterCriticalSection(&g_cs); g_n_Sum = 0; fot(int n=0;n<100;n++){ g_n_Sum += n; } LeaveCriticalSection(&g_cs); } //别的线程函数...注:
为了加入关键锁,需要使用下函数初始化关键段代码:
BOOL WINAPI InitializeCriticalSectionAndSpinCount( __inout LPCRITICAL_SECTION lpCriticalSection, __in DWORD dwSpinCount //旋转锁循环的次数,单处理器会忽略该参数 );
可以调用下函数来设置关键段的旋转次数:SetCriticalSectionSpinCount
如果有两个或以上的线程在同一时刻争夺关键段,关键段在内部会使用一个事件内核对象,该对象只有在第一次被使用时才被创建,并在函数DeleteCriticalSection调用时才被销毁。
3、Slim读写锁
SRWLock允许区分想要读取和更新的线程,可以让多个读线程进行读取,但是让写线程独占资源。
SRWLOCK srwLock; InitializeSRWLock(&srwLock); //写进程函数 DWORD WINAPI WriteThreadFuc(PVOID pvParam){ AcquireSRWLockExclusive(&srwLock); //更新资源 ReleaseSRWLockExclusive(&srwLock); } //读进程函数 DWORD WINAPI ReadThreadFuc(PVOID pvParam){ AcquireSRWLockShared(&srwLock); //读取资源 ReleaseSRWLockShared(&srwLock); }
4、条件变量
5、一些窍门和技巧:
应用程序的每个逻辑资源都因该有自己的锁,用来对逻辑资源的部分或整体的访问进行同步,不因该为所有的逻辑资源创建一个单独的锁。
如果需要同时访问多个逻辑资源,其每个逻辑资源都有自己的锁,那必须使用所有的锁才能以原子的方式完成操作:
DWORD WINAPI ThreadFuc(PVOID pvParam){ EnterCriticalSection(&g_cs1);//资源的锁 EnterCriticalSection(&g_cs2);//资源的锁 //访问资源 //访问资源 LeaveCriticalSection(&g_cs2); LeaveCriticalSection(&g_cs1); }
每个线程的线程函数在获得锁的顺序上必须一致,否则容易产生死锁,而LeaveCriticalSection的顺序则无关紧要,因为它不会让线程进入等待状态。
-------------------------------------------------------------
第九章、用内核对象进行线程同步
几乎所有内核对象都能进行同步,对线程同步来说,这些内核对象每一种要么处在触发(signaled)状态,要么处在未触发(nonsignaled)状态。
进程(线程)内核对象在创建时总处于未触发状态,当终止时,操作系统自动将其变为触发状态,当内核对象被触发后,它将永远保持触发状态,再也不会变回未触发状态。
可以有触发和未触发状态的内核对象有:进程、线程、作业、事件、信号量、互斥量、文件及控制台的标准输入流\输出流\错误流和可等待的计时器。
1、等待函数
等待函数:是一个线程自愿进入等待状态,直到指定的内核对象被触发为止。
DWORD WINAPI WaitForSingleObject( __in HANDLE hHandle, //要等待的内核对象 __in DWORD dwMilliseconds //愿意等待的最长时间,若为INFINITE则无限等待 );允许调用线程通时检查多个内核对象的触发状态的函数:
DWORD WINAPI WaitForMultipleObjects( __in DWORD nCount, //要检查的内核对象的数量 __in const HANDLE* lpHandles, //内核对象句柄数组 __in BOOL bWaitAll, //TRUE为所有内核对象触发才能调用 __in DWORD dwMilliseconds );
该函数会以原子方式执行所有的操作。
2、事件内核对象
事件内核对象包含:
自动重置事件:当线程成功等待到自动重置事件对象时,对象自动重置为未触发状态。
手动重置被触发时,正在等待该事件的多有线程都将变为可调度状态;自动重置事件被触发时,只有一个正在等待该事件的线程会变为可调度状态。
创建事件内核对象:
HANDLE WINAPI CreateEvent( __in_opt LPSECURITY_ATTRIBUTES lpEventAttributes, __in BOOL bManualReset, //手动重置为TRUE __in BOOL bInitialState, //初始化为触发状态TRUE __in_opt LPCTSTR lpName );Windows Vista提供了CreateEventEx,该函数更有用之处在于允许我们减少权限的方式打开一个已存在的事件。
HANDLE WINAPI CreateEventEx( __in_opt LPSECURITY_ATTRIBUTES lpEventAttributes, __in_opt LPCTSTR lpName, __in DWORD dwFlags, //接收两位掩码 __in DWORD dwDesiredAccess //指定在创建事件时返回的句柄对事件有何种访问权限 );
其它线程访问已存在事件:
其他函数:
SetEvent:将事件变为触发状态。
ResetEvent:将事件变为未触发状态
例:
HANDLE g_hEvent; DWORD WINAPI printfNum1(PVOID pvParam){ int * pNum = (int*)pvParam; DWORD dResult = WaitForSingleObject(g_hEvent,INFINITE); cout<<"printfNum1"<<endl; SetEvent(g_hEvent); return 0; } DWORD WINAPI printfNum2(PVOID pvParam){ int * pNum = (int*)pvParam; DWORD dResult = WaitForSingleObject(g_hEvent,INFINITE); cout<<"printfNum2"<<endl; SetEvent(g_hEvent); return 0; } int _tmain(int argc, _TCHAR* argv[]){ g_hEvent = CreateEvent(NULL,TRUE,FALSE,NULL); HANDLE hThread[2]; DWORD dwThreadID; int input; hThread[0] = CreateThread(NULL,0,printfNum1,&input,0,&dwThreadID); hThread[1] = CreateThread(NULL,0,printfNum2,&input,0,&dwThreadID); cin>>input; SetEvent(g_hEvent); char**pA = new char*[input]; pA[0] = new char[12]; WaitForMultipleObjects(2,hThread,TRUE,INFINITE);//等待两线程执行完 return 0; }
2、可等待的计时器内核对象
可等待的计时器:它们在会在指定的时间触发,或者每隔一段时间触发一次
创建可等待的计时器内核对象:
HANDLE WINAPI CreateWaitableTimer( __in_opt LPSECURITY_ATTRIBUTES lpTimerAttributes, __in BOOL bManualReset, //手动重置为TRUE __in_opt LPCTSTR lpTimerName );//在创建时对象总处于未触发状态
得到一个已存在的可等待的计时器的句柄:OpenWaitableTimer
触发计时器对象:
BOOL WINAPI SetWaitableTimer( __in HANDLE hTimer, //想要触发的计时器 __in const LARGE_INTEGER* pDueTime, //第一次触发的时间 __in LONG lPeriod, //第一次触发后以怎样的频度触发 __in_opt PTIMERAPCROUTINE pfnCompletionRoutine, __in_opt LPVOID lpArgToCompletionRoutine, __in BOOL fResume );
//把计时器第一次触发时间设为年月...,之后每小时触发一次 HANDLE hTimer; SYSTEMTIME st; FILETIME ftLocal,ftUTC; LARGE_INTEGER liUTC; hTimer = CreateWaitableTimer(NULL,FALSE,NULL); st.wYear = 2011; st.wMonth = 8; st.wDayOfWeek = 3; //... //SYSTEMTIME->FILETIME->LARGE_INTEGER SystemTimeToFileTime(&st,&ftLocal); LocalFileTimeToFileTime(&ftLocal,&ftUTC); liUTC.LowPart = ftUTC.dwLowDateTime; liUTC.HighPart = ftUTC.dwHighDateTime; SetWaitableTimer(hTimer,&liUTC,6*60*60*1000,NULL,NULL,FALSE);
3、信号量内核对象
信号量内核对象用来对资源进行计数它包含:
用信号量来监视资源并调度线程:当前资源初始化为0,随着服务不断接受客户请求,当前资源随之增加,随着服务器线程池接手处理客户请求,当前资源计数随之递减。
创建信号量内核对象:
HANDLE WINAPI CreateSemaphore( __in_opt LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, __in LONG lInitialCount, //当前资源计数 __in LONG lMaximumCount, //最大资源计数 __in_opt LPCTSTR lpName );
为了获得对保护资源的访问权,线程需要调用一个等待函数并传入信号量的句柄。在内部,等待函数会检查信号量的当前资源计数,如果其值大于0(触发状态),函数会将计数器减1并让调度线程执行。
信号量最大的优势是:它们以原子方式来执行测试和设置操作。
线程可以通过ReleaseSemaphore来增加信号量的当前资源计数。
4、互斥量内核对象
包含:
互斥量规则:
创建互斥量:
HANDLE WINAPI CreateMutex( __in_opt LPSECURITY_ATTRIBUTES lpMutexAttributes, __in BOOL bInitialOwner, //为FALSE时,互斥信号的线程ID和递归数都设为0,处于触发态 __in_opt LPCTSTR lpName );
CreateMutexEx , OpenMutex
为了获得对保护资源的访问权,线程需要调用一个等待函数并传入互斥量的句柄。在内部,等待函数会检查互斥量的线程ID是否为0,如果为零函数会将线程ID设为调用线程的ID,把递归计数设为1,让调用程序继续执行。
当目前占用访问权的线程不再需要访问资源时,必须调用ReleaseMutex函数释放互斥量,如果线程成功等待了互斥量多次,那么线程必须调用ReleaseMutex相同的次数才能使其递归计数归零。在线程调用ReleaseMutex时同样会比较互斥量内部的线程ID与调用线程的ID是否一致,如果不一致则无法释放。
如果拥有互斥量的线程在释放该互斥量之前终结了,这时系统认为互斥量被遗弃。等待一个被遗弃的的互斥量时,等待函数将返回WAIT_ABANDONED。
5、其他线程同步函数
1)、异步设备I/O
异步设备I/O允许线程开始读取操作和写入操作,但不必等待读取与写入操作完成。设备对象是可同步的内核对象。