作者:yurunsun@gmail.com 新浪微博@孙雨润 新浪博客 CSDN博客日期:2012年11月5日
1. 创建与终止线程
1.1 CreateThread函数
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);
【Note】CreateThread
函数是Windows API,不过在C/C++代码中绝对不要调用CreateThread
,而是_beginthreadex
。
1.2 终止运行线程
- 线程函数返回(Strong Recommanded)
- ExitThread 与 TerminateThread:造成C/C++资源泄露
1.3 C/C++运行时库注意事项
VS附带4个C/C++运行时库,和2个面向.NET的托管环境,所有都支持多线程开发。
- LibCMt.lib/libCMtD.lib 静态链接Release/Debug版
- MSVCRt.lib/MSVCRtD.lib 导入库,动态链接MSVCR90(D).dll
- MSVCMrt.lib 导入库,用于托管/本机代码混合
- MSVCURt.lib 导入库,编译100%MSIL代码
C运行时库在1970年发明时还没有thread的概念,全局变量在多线程环境中无法正常使用。必须创建一个数据结构,并使之与使用了C/C++运行库的每个线程关联,在调用C/C++运行时库函数时,那些函数必须知道去查找主调线程的数据块(_tiddata),避免影响其他线程。
但OS并不知道app是C/C++写的,因此不能调用OS提供的通用线程创建函数,而要调用C/C++运行时库函数_beginthreadex
。
调用CreateThread
的后果:当一个线程调用需要_tiddata
的C/C++运行库函数,库函数会尝试调用TlsGetValue
获取地址,失败时会为主调线程分配一个_tiddata
,并通过TlsSetValue
关联。问题是:如果线程使用了C/C++运行库的signal
函数,则整个进程会终止,因为异常处理帧没有就绪;如果线程不通过_endthreadex
来终止,_tiddata
不会被释放。
2. 伪句柄与真实句柄
-
伪句柄
为了避免获取内核对象时引用计数频繁加减,OS提供一些函数来方便引用它的进程/线程内核对象。
HANDLE GetCurrentProcess(); HANDLE GetCurrentThread();
它本身就只指向调用它的主调进程或线程,会因为调用者的不同而改变:调用者A使用一个伪句柄,这个句柄指向调用者A,而调用者A将该句柄传递给调用者X,则这个句柄就指向调用者X。通过调试的方式查看伪句柄得知,进程总是0xffffffff,而线程总是0xfffffffe。
-
真实句柄
DWORD GetCurrentProcessId(); DWORD GetCurrentThreadId();
-
伪句柄转真实句柄
HANDLE hThreadParent; DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &hThreadParent, 0, FALSE, DUPLICATE_SAME_ACCESS);
因为
DuplicateHandle
递增了指定内核对象的使用计数,记得用CloseHandle
关闭目标句柄。
3. 线程调度
3.1 挂起和恢复
- 线程内核对象中有一个值表示线程的挂起计数,初始化时默认为1
CreateProcess/CreateThread
时传入CREATE_SUSPENDED
会使线程完成初始化之后继续挂起ResumeThread
函数用于线程恢复- 一个线程的最大挂起深度为
MAXIMUM_SUSPEND_COUNT
3.2 Sleep() 与 SwitchToThread()
- 调用
Sleep
,线程自愿放弃属于它时间片中剩余部分 Sleep
时间只是近似,因为Windows不是实时操作系统Sleep
的dwMs
传入INFINITE
,告诉OS永远不要调用这个线程,无意义Sleep
的dwMs
传入0,告诉OS放弃时间片剩余部分;但如果其余线程优先级都小于当前线程,会被立即重新调度SwitchToThread()
与Sleep()
的区别是,允许切换到低优先级线程
3.3 优先级编程
CreateProcess
时可以再fdwCreate参数中传入需要的优先级: real-time/high/above normal/normal/below normal/idle-
进程运行之后改变自己的优先级:
SetPriorityClass(GetCurrentProcess(), IDLE_PRIORITY_CLASS);
-
获取优先级的API:
GetPriorityClass(GetCurrentProcess());
-
Cmd中用Start命令启动应用程序,可以指定优先级:
C:\>start -low calc.exe
-
CreateThread
无法指定线程优先级,总是normal -
线程运行后改变优先级:
DWORD dwThreadID; HANDLE hThread = _beginthreadex(NULL, 0, ThreadFunc, NULL, CREATE_SUSPENDED, &dwThreadID); SetThreadPriority(hThread); ResumeThread(hThread); CloseHandle(hThread);
-
获取线程优先级:
GetThreadPriority(GetCurrentThread());
-
动态提升线程优先级:OS通过thread的相对优先级+thread所属process的优先级来确定thread的优先级值,叫做thread的base priority level. 为了响应I/O,OS会偶尔提升一个thread的优先级,I/O处理结束后降会base level. 使用以下两个API能禁止动态提升优先级:
BOOL SetProcessPriorityBoost(HANDLE hProcess, BOOL bDisablePriorityBoost); BOOL SetThreadPriorityBoost(HANDLE hProcess, BOOL bDisablePriorityBoost); BOOL GetProcessPriorityBoost(HANDLE hProcess, PBOOL pbDisablePriorityBoost); BOOL GetThreadPriorityBoost(HANDLE hProcess, PBOOL pbDisablePriorityBoost);
-
进程/线程的CPU亲和性:OS给thread分配CPU时默认使用soft affinity,即如果其他因素一样,OS将使thread在上一次运行的CPU上运行。限制一个进程的所有thread只在CPU的一个子集上运行:
BOOL SetProcessAffinityMask(HANDLE hProcess, DWORD_PTR dwProcessAffinityMask); BOOL GetProcessAffinityMask(HANDLE hProcess,PDWORD_PTR lpProcessAffinityMask,PDWORD_PTR lpSystemAffinityMask);
子进程将继承CPU亲和性。限制单个线程的API:
DWORD_PTR SetThreadAffinityMask(HANDLE hThread, DWORD_PTR dwThreadAffinityMask);
如果在设置线程CPU亲和性的同时,允许OS调度到idle CPU,使用API:
DWORD SetThreadIdealProcessor(HANDLE hThread, DWORD dwIdealProcessor);
4. 用户态线程同步
4.1 使用Interlocked系列函数进行原子访问
Interlocaked系列函数执行的极快,调用一次通常只占几个CPU Cycle,而且不需要用户态内核态切换——这个切换需要占用1000个Cycle以上。
-
InterlockedExchangeAdd代替简单的C/C++语句修改共享变量:
LONG g_x; g_x--; // Wrong! InterlockedExchangeAdd(&g_x, -1); // Correct!
-
InterlockedExchange/InterlockedExchangePointer实现spinlock:
BOOL g_fResourceInUse = FALSE; while (InterlockedExchange(&g_fResourceInUse, TRUE) == TRUE) { Sleep(0); } // Access the resource... // We no longer need to access the resource InterlockedExchange(&g_fResourceInUse, FALSE);
这两个API会把第一个参数所指向的mem addr的当前值,以原子方式替换为第二个参数指定的值(后者在64位系统会替换64位值),并返回原来的值。while循环把g_fResourceInUse设置为TRUE并检查原来值是否为TRUE:如果原来为FALSE则说明资源未被使用,调用线程立即就能将其设为TRUE并退出循环;如果原来为TRUE,说明有其他线程正在使用该资源,于是while循环继续执行。
-
InterlockedCompareExchange/InterlockedCompareExchangePointer
- InterlockedIncrement/InterlockedDecrement
4.2 使用CriticalSection进行多行代码原子操作
- 使用方法:当有一个资源要让多个thread访问时,应该创建一个
CRITICAL_SECTION
结构,无论哪里代码要访问一个资源都必须调用EnterCriticalSection
并传入CRITICAL_SECTION
的地址,当thread不在需要访问该资源时,调用LeaveCriticalSection
。 -
使用条件:任何线程试图访问被保护的资源之前,必须对
CRITICAL_SECTION
结构的内部成员初始化:VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);
当线程不需要访问共享资源时,调用:
VOID DeleteCriticalSection(PCRITICAL_SECTION pcs);
-
实现方式:如果没有线程正在访问资源,
EnterCriticalSection
会更新成员变量并立即返回,线程可以继续执行;如果成员变量表示有另外线程已经获准访问资源,那么EnterCriticalSection
会使用一个事件内核对象把调用thread切换到Pending状态,不浪费CPU。OS会记住这个thread想访问这个资源,一旦当前正在访问资源的thread调用了LeaveCriticalSection
,OS会自动更新成员变量,将Pending thread切回可调度状态。 -
避免过长时间Pending:
BOOL TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
从不会让调用线程进入Pending状态,而通过返回值表示是否获准访问资源。如果此API返回TRUE,则
CRITICAL_SECTION
已经更新过了,表示线程正在访问资源,因此必须调用LeaveCriticalSection
。 -
配合SpinLock:切换到Pending状态意味着thread必须从用户态切换到内核态(大约1K个CPU Cycle),开销非常大;为提高CriticalSection的性能,OS合并了SpinLock,当调用
EnterCriticalSection
时会尝试在一段时间内使用SpinLock,超时才会切换到内核模式并Pending。使用这个功能需要调用下面API初始化CriticalSection:BOOL InitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION lpCriticalSection, DWORD dwSpinCount); DWORD SetCriticalSectionSpinCount(LPCRITICAL_SECTION lpCriticalSection, DWORD dwSpinCount);
第二个参数是我们希望SpinLock循环的次数。在单CPU机器上会将这个参数全当成0对待。
4.3 Slim读写锁
与CriticalSection不同的是,SRWLock区分读取者thread和写入者thread,让所有读取者thread同一时刻访问共享资源是可行的,只有当写入者thread想要对资源更新时候才需要同步。这时候写入者thraed独占对资源的访问权。
-
首先需要分配一个
SRWLOCK
结构并用InitializeSRWLock
函数初始化:VOID InitializeSRWLock(PSRWLOCK SRWLock);
-
SRWLock
初始化完成后,写入者thread就可以调用:VOID AcquireSRWLockExclusive(PSRWLOCK SRWLock); VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
-
读取者thread同样有两个步骤:
VOID AcquireSRWLockShared(PSRWLOCK SRWLock); VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);
-
与CriticalSection相比SRWLock缺乏以下两个特性:不存在
TryEnter*
函数,如果锁被占用一定Pending;不能递归地获得SRWLOCK
【Note】从性能角度:首先尝试不用共享数据,然后使用volatile读写,Interlocked API,SRWLock以及CriticalSection,最后使用接下来介绍的内核对象。
5. 内核对象进行线程同步
与用户态的同步机制相比,内核对象的用途更广泛,唯一缺点就是性能。
5.1 等待函数
等待函数使一个线程自愿进入Pending状态,直到指定的内核对象被触发为止。如果调用时相应内核已经处于触发状态,线程不会进入Pending状态。
-
DWORD WaitForSingleObject(HANDLE hObject, DWORD dwMilliseconds);
DWORD dw = WaitForSingleObject(hProcessT, 5000); switch (dw) { case WAIT_OBJECT_0: // The process terminated break; case WAIT_TIMEOUT: // The process didn't terminate within 5000 ms break; case WAIT_FAILED: // Bad call to function (invalid handle?) break; }
-
DWORD WaitForMultipleObjects(DWORD nCount, const HANDLE* lpHandles, BOOL bWaitAll, DWORD dwMilliseconds);
与上面唯一不同时能同时检查最多
MAXIMUM_WAIT_OBJECTS
个内核对象。bWaitAll表示等待所有内核对象还是其中任一内核对象触发,触发时返回值为WAITOBJECT0 + 触发对象的下标。HANDLE h[3] = {hProcess1, hProcess2, hProcess3}; DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000); switch (dw) { case WAIT_FAILED: break; case WAIT_TIMEOUT: break; case WAIT_OBJECT_0 + 0: break; // h[0] case WAIT_OBJECT_0 + 1: break; // h[1] case WAIT_OBJECT_0 + 2: break; // h[2] }
5.2 Event内核对象 —— 最基本的事件
-
Event包含一个使用计数、一个用来表示自动重置还是手动重置的BOOL、一个用来表示是否触发的BOOL。手动重置Event被触发时,正在等待该Event的所有thread都将变成可调度状态;自动重置Event被触发时,只有一个正在等待该Event的thread会变成可调度状态。
-
API:
HANDLE CreateEvent(PSECURITY_ATTRIBUTES psa, BOOL bManualReset, BOOL bInitialState, PCTSTR pszName); HANDLE OpenEvent(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName); BOOL SetEvent(HANDLE hEvent); BOOL ResetEvent(HANDLE hEvent);
-
Example:
HANDLE g_hEvent; int WINAPI _tWinMain(...) { g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); HANDLE hThread[3]; DWORD dwThreadID; hThread[0] = _beginthreadex(NULL, 0, WordCount, NULL, 0, &dwThreadID); hThread[1] = _beginthreadex(NULL, 0, SpellCheck, NULL, 0, &dwThreadID); hThread[2] = _beginthreadex(NULL, 0, GrammarCheck, NULL, 0, &dwThreadID); OpenFileAndReadContentsIntoMemory(...); SetEvent(g_hEvent); } DWORD WINAPI WordCount(PVOID pvParam) { WaitForSingleObject(g_hEvent, INFINITE); // Access the Memory block... return 0; } DWORD WINAPI SpellCheck(PVOID pvParam) { WaitForSingleObject(g_hEvent, INFINITE); // Access the Memory block... return 0; } DWORD WINAPI GrammarCheck(PVOID pvParam) { WaitForSingleObject(g_hEvent, INFINITE); // Access the Memory block... return 0; }
5.3 WaitableTimer内核对象 —— 定时器
- WaitableTimer会在某个指定的时间出发,或每隔一段时间触发一次,通常用来在某个时间执行一些操作。
-
API:
HANDLE CreateWaitableTimer(LPSECURITY_ATTRIBUTES lpTimerAttributes, BOOL bManualReset, LPCTSTR lpTimerName); HANDLE OpenWaitableTimer(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpTimerName); BOOL SetWaitableTimer(HANDLE hTimer, const LARGE_INTEGER *pDueTime, LONG lPeriod, PTIMERAPCROUTINE pfnCompletionRoutine, LPVOID lpArgToCompletionRoutine, BOOL fResume);
-
Example:
HANDLE hTimer; SYSTEMTIME st; FILETIME ftLocal, ftUTC; LARGE_INTEGER liUTC; //Create an auto-reset hTimer = CreateWaitableTimer(NULL, FALSE, NULL); // First signaling is at 2012-11-11 1:00 PM st.wYear = 2012; st.wMonth = 11; st.wDayOfWeek = 0; st.wDay = 11; st.wHour = 13; st.wMinute = 0; st.wSecond = 0; st.wMilliseconds = 0; // Convert to UTC SystemTimeToFileTime(&st, &ftLocal); LocalFileTimeToFileTime(&ftLocal, &ftUTC); liUTC.LowPart = ftUTC.dwLowDateTime; liUTC.HighPart = ftUTC.dwHighDateTime; // Set the timer event SetWaitableTimer(hTimer, &liUTC, 7*60*60*1000, NULL, NULL, FALSE);
-
两种回调方式:WaitFor[Single|Multiple]Object 和计时器APC调用前者不再累述,后者则是把一个APC(异步过程调用)放到
SetWaitableTimer
的调用线程的队列中。在调用SetWaitableTimer
时一般倒数2、3参数传NULL,表示时间一到触发计时器对象即可;但如果传入一个计时器APC函数地址,则可以把这个APC添加到队列中去:VOID TimerAPCRoutine(PVOID pvArgToCompletionRoutine, DWORD dwTimerLowValue, DWORD dwTimerHighValue) { // Do whatever you wanner } Handle hTimer = CreateWaitableTimer(NULL, TRUE, NULL); LARGE_INTEGER li = {0}; SleepEx(INFINITE, TRUE); CloseHandle(hTimer);
【Note】线程不能同时使用这两种方式等待同一计时器!
5.4 Semaphore内核对象 —— 计数器
-
Semaphore用来对资源计数,除了所有内核对象共有的使用计数外,还包含最大资源计数、当前资源计数。
【Note】使用计数是内核对象自身的计数,不要与资源计数混淆。
-
规则: 如果当前资源计数>0,Semaphore触发;如果当前资源计数=0,Semaphore不触发;当前资源计数介于0与最大资源计数之间;触发时(即资源计数>0)每执行一次
WaitForSingleObject
线程不阻塞,资源计数减一;不触发时调用线程陷入Pending状态,等待其他threadReleaseSemaphore
-
API:
// 创建Semaphore HANDLE CreateSemaphoreEx( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName, DWORD dwFlags, DWORD dwDesiredAccess ); // 打开已有Semaphore HANDLE OpenSemaphore(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName); // 增加一定数量的信号量 BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount);
-
与其他内核对象不同点:
假如thread试图等待一个未触发的内核对象,通常会使thread陷入Pending状态;但对于Mutex,OS会检查调用thread的ID与Mutex内部记录的thread ID是否相同,如果相同OS则不会让thread陷入Pending,无论Mutex是否触发。多次调用WaitForSingleObject(hMutex)会导致递归数递增,需要相同次数的
ReleaseMutex
才会使对象重新触发。Mutex是唯一具有“线程所有权”概念的内核对象,其他内核对象不会记住自己是哪个thread等待成功。带来的遗弃问题不仅表现在同一thread多次WaitFor,还适用于试图释放Mutex的thread。
ReleaseMutex
时只有调用线程与OwnerThread的ID相同才会成功,否则会直接返回FALSE,GetLastError
会得到ERROR_NOT_OWNER
,表示试图释放的Mutex不属于调用thread。