作者:[email protected] 新浪微博@孙雨润 新浪博客 CSDN博客日期:2012年11月5日
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
。
VS附带4个C/C++运行时库,和2个面向.NET的托管环境,所有都支持多线程开发。
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
不会被释放。
伪句柄
为了避免获取内核对象时引用计数频繁加减,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
关闭目标句柄。
CreateProcess/CreateThread
时传入CREATE_SUSPENDED
会使线程完成初始化之后继续挂起ResumeThread
函数用于线程恢复MAXIMUM_SUSPEND_COUNT
Sleep
,线程自愿放弃属于它时间片中剩余部分Sleep
时间只是近似,因为Windows不是实时操作系统Sleep
的dwMs
传入INFINITE
,告诉OS永远不要调用这个线程,无意义Sleep
的dwMs
传入0,告诉OS放弃时间片剩余部分;但如果其余线程优先级都小于当前线程,会被立即重新调度SwitchToThread()
与Sleep()
的区别是,允许切换到低优先级线程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);
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
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对待。
与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,最后使用接下来介绍的内核对象。
与用户态的同步机制相比,内核对象的用途更广泛,唯一缺点就是性能。
等待函数使一个线程自愿进入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]
}
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;
}
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】线程不能同时使用这两种方式等待同一计时器!
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。