Windows系统编程(三):线程

作者:[email protected] 新浪微博@孙雨润 新浪博客 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不是实时操作系统
  • SleepdwMs传入INFINITE,告诉OS永远不要调用这个线程,无意义
  • SleepdwMs传入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。

  • 如果这篇文章对您有帮助,请到CSDN博客留言;
  • 转载请注明:来自雨润的技术博客 http://blog.csdn.net/sunyurun

你可能感兴趣的:(Windows系统编程(三):线程)