第八章用户模式下的线程同步
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); } //别的线程函数...
注:
- 关键段内部使用的是Interlocked函数,所以速度很快,但是它无法对多个进程之间的线程进行同步。
- 关键段使用前必须用InitializeCriticalSection初始化,使用后用DeleteCriticalSection删除。
- 可以使用TryEnterCriticalSection,该函数不会让线程进入返回状态,它通过返回值来表示调用线程是否获准访问资源,如果不允许访问,线程还可以做别的事而不会等待。
- 使用完资源后要调用LeaveCriticalSection函数,该函数也是以原子的方式进行检测和更新操作,但是它不会让线程进入等待状态。
- 含有旋转锁的关键段:可以将旋转锁和关键段结合使用,在线程访问关键段代码时,先使用旋转锁不断的循环尝试一段时间获得对资源的访问权,尝试失败时才切换为等待状态(切换开销大)。
为了加入关键锁,需要使用下函数初始化关键段代码:
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);
}
- 如果锁被占用,那么调用AcquireSRWLock(Shared/Exclusive)会阻塞调用线程
- 不能递归的获得SRWLOCK
- 如果希望在应用程序中得到最佳的性能,首先应该尝试使用共享数据,然后一次使用volatile读取,volatile写入,Interlocked API ,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 //指定在创建事件时返回的句柄对事件有何种访问权限
);
其它线程访问已存在事件:
- 调用CreateEvent并为pszName参数传入相同的值
- 使用继承
- 使用DuplicateHandle函数
- 调用OpenEvent函数,并在pszName中指定与CreateEvent中相同的名字
其他函数:
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);
- 可以指定一个相对的时间,只要在pDueTime参数传入一个100纳秒的整数倍的负值。
- 可以让计时器指出发一次,之后不再触发,只要给lPeriod参数传0
3、信号量内核对象
信号量内核对象用来对资源进行计数它包含:
- 使用计数器
- 最大资源计数
- 当前资源计数
用信号量来监视资源并调度线程:当前资源初始化为0,随着服务不断接受客户请求,当前资源随之增加,随着服务器线程池接手处理客户请求,当前资源计数随之递减。
创建信号量内核对象:
HANDLE WINAPI CreateSemaphore(
__in_opt LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
__in LONG lInitialCount, //当前资源计数
__in LONG lMaximumCount, //最大资源计数
__in_opt LPCTSTR lpName
);
为了获得对保护资源的访问权,线程需要调用一个等待函数并传入信号量的句柄。在内部,等待函数会检查信号量的当前资源计数,如果其值大于0(触发状态),函数会将计数器减1并让调度线程执行。
信号量最大的优势是:它们以原子方式来执行测试和设置操作。
线程可以通过ReleaseSemaphore来增加信号量的当前资源计数。
4、互斥量内核对象
包含:
- 使用计数器
- 线程ID:标识当前占用这个互斥量的是系统中的那个线程
- 递归计数 :该线程占用互斥量的次数
互斥量规则:
- 如果线程ID为0,互斥量不为任何线程占用,为触发状态,非零则为非触发状态
- 与所有其他内核对象不同,操作系统对互斥量进行了特殊处理:假设线程试图等待一个未触发的互斥量,如果获得互斥量的线程ID与互斥量内部记录的线程ID一致,那么系统会让线程保持可调度状态,即使互斥量尚未触发。(互斥量内核对象独有的特性)
创建互斥量:
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允许线程开始读取操作和写入操作,但不必等待读取与写入操作完成。设备对象是可同步的内核对象。