windwos核心编程读书笔记

来源:互联网 发布:写钢琴谱的软件 编辑:程序博客网 时间:2024/05/18 05:31

windwos核心编程读书笔记5——线程(5)用户态线程同步

在多线程环境下,线程同步是不可避免的话题。Windows环境下的线程同步分为:用户态同步 与 内核态同步。

下面我们先了解用户态同步的一些方法。

  • 使用Interlocked系列函数。简单易用的函数。
  • 关键段。用来对关键资源实现独享访问。
  • Slim读写锁。灵活的进行写独享读共享操作。
  • 条件变量。当线程要进行较为复杂的条件进行同步时,可以实现。

Interlocked系列函数。

Windows提供了Interlocked系列函数,用来原子的对数据进行增减或交换。如自旋转锁,就可以通过InterlockedExchange函数实现。

BOOL g_fResourceInUse = FALSE;void Func1(){    // InterlockedExchange会一直原子设置g_fResourceInUse为TRUE,同时返回g_fResourceInUse    // 上一次的值。当g_fResourceInUse为初始值FALSE或被另一线程设置为FALSE后,该循环才会结束。    // 以此自循环(自旋锁)方式来实现对资源的独占访问。    while (InterlockedExchange(&g_fResourceInUse,TRUE) == TRUE)    Sleep(0);    // Access the resource, do something    ...    // Do not need the resource anymore, release it    InterlockedExchange(&g_fResourceInUse,FALSE)}

注意,这种循环方式会占用CPU大量时间,不建议在单CPU机器上运行。(可以用关键段代替或用C++11标准中的atom系列函数代替)


关键段

上面使用自旋锁的方式进行同步显然是低效的。因为等待线程依然处于可调度状态,仍然会占用CPU时间。Windows提供了一系列函数,让线程同步。这一系列函数保证了在线程获得想要的资源之前,不被CPU调度,直到其要求的资源可被线程访问为止。关键段就是其一,其实关键段的实现是通过事件内核对象的。

运用关键段五个步骤:

1、声明一个可以被多个线程访问到其地址的关键段变量。

2、在使用关键段前,调用InitializeCriticalSection函数初始化关键段。

2、在进入资源前调用EnterCriticalSection,请求进入关键段(若进入不了,则线程等待)

3、在离开资源时,调用LeaveCriticalSection,离开关键段。


若确定了关键段已经不被任何线程再使用,则要销毁关键段对象。

4、在不再使用关键段时,调用DeleteCriticalSection销毁关键段。

CRITICAL_SECTION g_cs;int g_sum = 0;//初始化关键段,注意不要多次初始化,否则后果是未定义的<pre name="code" class="cpp">InitializeCriticalSection(&g_cs);

void ThreadFunc1()
{
    
    EnterCriticalSection(&g_cs);
    g_sum++;
    LeaveCriticalSection(&g_cs);
}

void ThreadFunc2()
{
    
    EnterCriticalSection(&g_cs);
    g_sum++;
    LeaveCriticalSection(&g_cs);
}
...
...
// 不再使用critical section,显示销毁
DeleteCriticalSection(&g_cs);

关键段最容易忘记
LeaveCriticalSection
,这时候可以用RAII技巧来进行简单的封装。

关于关键段的细节

1、若一个线程已经成功进入关键段,则可以多次调用EnterCriticalSection,相应的,要调用多次LeaveCriticalSection来离开临界区。

2、对于跨进程的线程同步,可以使用mutex对象。


可以使用TryEnterCriticalSection进入关键段,他不会使线程进入等待,而是返回布尔值表示是否获得了关键段。对于返回TRUE,需要调用LeaveCriticalSection。


关键段与旋转锁

当线程由于得不到关键段而进入等待状态时,会进行用户态和内核态切换,这会占用大量的CPU时间。在多处理器的环境下,可能的一种情况是,用户/内核态的切换还未结束,占用关键段的线程可能已经释放了关键段。

在多处理器的情况下,可以使用

InitializeCriticalSectionAndSpinCount

函数来初始化关键段。其函数原型如下

BOOL WINAPI InitializeCriticalSectionAndSpinCount(  _Out_  LPCRITICAL_SECTION lpCriticalSection,  _In_   DWORD dwSpinCount);

其中参数dwSpinCout用来设置旋转锁循环次数。SetCriticalSectionSpinCount 可以重设自旋转锁次数。

该函数会在进入内核态前,旋转设置的循环次数来获取关键段。若在旋转锁阶段获取关键段,则不会进入内核态。

注意,在单CPU模式下,dwSpinCout是被忽略的,总是为0。因为在单CPU下,

InitializeCriticalSectionAndSpinCount
是没有意义的:CPU在旋转锁阶段被线程占用,其他线程根本没有时机来释放关键段。但我们仍可以这样初始化关键段,以应对未来可能的多CPU环境。


Slim读写锁

一般的,对于线程的同步,读是可以共享的,而写则是互斥的。因此Windows提供了读写锁机制。

与关键段类似,在使用读写锁之前,要调用InitializeSRWLock函数初始化读写锁。

利用读写锁要分清读者和写者。

读写锁使用步骤

1、声明SRWLOCK对象。

2、用InitializeSRWLock函数初始化SRWLOCK对象。

3.1、对于读者,调用

AcquireSRWLockShared

ReleaseSRWLockShared

以共享的方式获取,释放的读写锁。若该锁没被占用或被其他线程读,则立即获得锁,否则等待。

3.2、对于写者,调用

AcquireSRWLockExclusive

ReleaseSRWLockExclusive

以独占的方式获取,释放的读写锁。若该锁未被占用,则立即获得锁,否则等待。

Slim与关键段的对比

Slim锁与关键段主要有以下两点区别

1、Slim锁不能够递归获取,即当一个线程Acquire并获得Slim锁之后,不能够再次Acquire同一把锁。

2、不存在TryEnter类似函数获取Slim锁。

3、Slim锁不用显示销毁,系统会自动释放。

4、总体上说,Slim锁的效率优于关键段。


多种同步方法的效率对比


条件变量同步

有时候需要线程原子方式释放获得的锁同时阻塞自身,直到某一条件成立为止。这时候可以通过条件变量进行同步。

等待条件变量函数。当条件被满足,线程被唤醒后,会自动得到锁。

SleepConditionVariableCS

SleepConditionVariableSRW

唤醒等待条件的线程函数

WakeAllConditionVariable

WakeConditionVariable

windows核心编程读书笔记6——内核态线程同步(1)概述

除了用户态的线程同步,我们可以使用内核对象进行线程的同步。与用户态同步相比,内核态同步耗时要多(用户态内核态切换),但内核同步能够跨进程同步,并使用灵活,以及配套的安全机制。

触发未触发状态

对于内核对象来说,均有触发及未触发状态,其状态转换规则因不同内核对象而异。利用内核对象同步,我们就利用了内核对象的这种状态转换。

等待函数

要进行内核态同步,需要使用等待函数来使为获得等待对象触发状态的线程处于等待状态。常用的等待函数有两个:

等待单个内核对象:

DWORD WINAPI WaitForSingleObject(  _In_  HANDLE hHandle,       // 等待的内核对象句柄  _In_  DWORD dwMilliseconds  // 以毫秒计数的等待超时,INFINITE为无限等待);
该函数的返回值有以下4种

WAIT_ABANDONED0x00000080LThe specified object is a mutex object that was not released by the thread that owned the mutex object before the owning thread terminated. Ownership of the mutex object is granted to the calling thread and the mutex state is set to nonsignaled.If the mutex was protecting persistent state information, you should check it for consistency.WAIT_OBJECT_00x00000000LThe state of the specified object is signaled.WAIT_TIMEOUT0x00000102LThe time-out interval elapsed, and the object's state is nonsignaled.WAIT_FAILED(DWORD)0xFFFFFFFFThe function has failed. To get extended error information, call GetLastError.

等待多个内核对象:

DWORD WINAPI WaitForMultipleObjects(  _In_  DWORD nCount,              // 等待内核对象数目  _In_  const HANDLE *lpHandles,   // 等待内核对象数组  _In_  BOOL bWaitAll,             // 是否等待所有的内核对象才唤醒线程  _In_  DWORD dwMilliseconds       // 等待超时时间);

其可能返回的结果

WAIT_OBJECT_0 to (WAIT_OBJECT_0 + nCount– 1)If bWaitAll is TRUE, the return value indicates that the state of all specified objects is signaled.If bWaitAll is FALSE, the return value minus WAIT_OBJECT_0 indicates the lpHandles array index of the object that satisfied the wait. If more than one object became signaled during the call, this is the array index of the signaled object with the smallest index value of all the signaled objects.WAIT_ABANDONED_0 to (WAIT_ABANDONED_0 + nCount– 1)If bWaitAll is TRUE, the return value indicates that the state of all specified objects is signaled and at least one of the objects is an abandoned mutex object.If bWaitAll is FALSE, the return value minus WAIT_ABANDONED_0 indicates the lpHandles array index of an abandoned mutex object that satisfied the wait. Ownership of the mutex object is granted to the calling thread, and the mutex is set to nonsignaled.If a mutex was protecting persistent state information, you should check it for consistency.WAIT_TIMEOUT0x00000102LThe time-out interval elapsed and the conditions specified by the bWaitAll parameter are not satisfied.WAIT_FAILED(DWORD)0xFFFFFFFFThe function has failed. To get extended error information, call GetLastError.

关于等待函数有两点应该知道的地方

1、对于一种特殊的内核对象,即mutex对象,其存在互斥量遗弃现象。即,若获得mutex的线程终止,却没有显示的释放mutex,那么系统自动会默认原线程遗弃了mutex,并自动在等待该mutex对象的剩余线程中,挑选一个进行唤醒。而这时,wait函数的返回值不是WAIT_OBJECT(_0),而是WAIT_ABANDONED(_0)。

2、我们可以运用wait函数的扩展

WaitForSingleObjectEx

WaitForMultipleObjectsEx

将等待线程设置为可提醒状态,这样除了线程等待的内核对象外,我们可以在用户模式下线程的异步调用队列(APC)中,加入信号来唤醒线程(此时不要求等待内核对象是触发的,wait函数会返回WAIT_IO_COMPLETION)。

类似的有SleepEx函数。


等待成功的副作用

如果wait函数等待成功了内核对象后,会改变内核对象的状态,这就称作为等待成功的副作用。并不是所有的内核对象都有副作用,如进程和线程内核对象没有任何副作用。而对于event内核对象,如果我们将event事件的重置状态设置为自动重置,那么在一个线程获得内核对象之前,系统会将该event自动重置为未触发状态(等待成功的副作用)。这样,在多个等待该事件的线程中,仅有一个会被唤醒

windows核心编程读书笔记7——内核态线程同步(2)利用内核对象同步

事件对象

event对象常用来多个线程间进行工作的同步,如线程A先执行一些初始化工作,触发evnet,通知线程B初始化工作已经完成,可以进行接下来的工作。

创建event对象

HANDLE WINAPI CreateEvent(  _In_opt_  LPSECURITY_ATTRIBUTES lpEventAttributes,  // 设置安全属性  _In_      BOOL bManualReset,                        // 是否人工重置状态(人工重置则不会自动改变事件状态, 自动重置则会自动将事件恢复为未触发)  _In_      BOOL bInitialState,                       // 事件初始状态(触发/未触发)  _In_opt_  LPCTSTR lpName                            // 事件名称);

值得注意的是
<span style="color:#FF0000;">BOOL bManualReset, </span> 

若 为人工重置,那么当事件触发时,所有等待线程均能够获得事件对象,且不会自动重置事件状态。若为自动重置,则仅有一个线程wait获得该事件,同时置事件为未触发状态。

另外一点,其他线程若想获得该事件对象句柄,可以也调用CreateEvent函数,并传入事件名称。若该事件已经存在,则直接返回句柄,若未存在则会创建该事件并返回句柄。

注意,若事件已经存在,再调用CreateEvent只会获取其句柄,但该函数的其他参数会忽略

对于自动重置事件,若multiplewait函数为全部等待状态,则对于仅自动重置事件触发时,multiplewait函数会忽略该event,同时不会重置事件,只有当所有的等待对象都触发时,multiplewait才会获取自动重置事件并自动重置为未触发状态。

若想在创建事件时指定的可以访问事件的权限,可以用

HANDLE WINAPI CreateEventEx(  _In_opt_  LPSECURITY_ATTRIBUTES lpEventAttributes,  _In_opt_  LPCTSTR lpName,  _In_      DWORD dwFlags,                             // 可以是两种flags的任意组合<strong>CREATE_EVENT_INITIAL_SET</strong>、<strong>CREATE_EVENT_MANUAL_RESET</strong>  _In_      DWORD dwDesiredAccess                      // 设置事件权限);

获取事件句柄函数

HANDLE WINAPI OpenEvent(  _In_  DWORD dwDesiredAccess,  _In_  BOOL bInheritHandle,  _In_  LPCTSTR lpName);

改变事件触发状态

设置事件为触发状态

BOOL WINAPI SetEvent(  _In_  HANDLE hEvent);

设置事件未触发状态

BOOL WINAPI ResetEvent(  _In_  HANDLE hEvent);

可等待计时器内核对象

可等待计时器对象会在一定时后或每间隔一段时间触发,可用在某个时间的操作。

创建或获取可等待计时器

HANDLE WINAPI CreateWaitableTimer(  _In_opt_  LPSECURITY_ATTRIBUTES lpTimerAttributes,  _In_      BOOL bManualReset,                     // 是否人工重置  _In_opt_  LPCTSTR lpTimerName);

获取可等待计时器句柄

HANDLE WINAPI OpenWaitableTimer(  _In_  DWORD dwDesiredAccess,  _In_  BOOL bInheritHandle,  _In_  LPCTSTR lpTimerName);

不像事件对象,可等待计时器创建后总是未触发的

需要调用函数 SetWaitableTimer

BOOL WINAPI SetWaitableTimer(  _In_      HANDLE hTimer,                          // 计时等待对象  _In_      const LARGE_INTEGER *pDueTime,          // 何时触发对象(用负值表示相对于调用SetWaitableTimer后的时间 100纳秒为单位)  _In_      LONG lPeriod,                           // 触发后间隔的触发频率(0 表示仅触发一次)  _In_opt_  PTIMERAPCROUTINE pfnCompletionRoutine,  // APC调用函数  _In_opt_  LPVOID lpArgToCompletionRoutine,        // APC调用参数  _In_      BOOL fResume                            // 在可挂起的计算机系统中,是否恢复计算机来使等待线程执行CPU时间。                                                    // 若传入FALSE,则会触发对象,但等待线程不会执行,除非直到计算机系统重新执行 );

取消计时等待对象的时间设置

该函数会取消一切的SetWaitableTimer的计时设置。<strong>但是该函数不会更改timer对象的触发状态</strong>,若已经触发,则该对象仍会处于触发状态。BOOL WINAPI CancelWaitableTimer(  _In_  HANDLE hTimer);

计时等待对象 VS 用户计时器(SetTimer)

1、内核对象,用户对象

2、用户计时器会产生WM_TIMER消息,该消息会被送到调用SetTimer线程或创建窗口线程,同一时间仅有一个线程得到通知。

计时等待对象可多个线程同时被通知。

信号量内核对象

信号能够灵活的限制可被激活的线程数目,并确保线程数目不会超过设定的最大值。

具体使用流程为:

1、创建信号量对象,并指定最大资源数目与当前可用数目(常为0)。

2、创建多个资源请求线程,因为当前可用数目为0,线程等待。

3、当符合某种条件时,调用ReleaseSemaphore函数释放资源,这时候可用资源数目递增。

4、可以资源数目不再为0,等待线程获得资源,同时可以资源数目递减。


windows系统会确保当前可用资源数目大于等于0,同时不会超过最大值。


创建(或获取)信号量对象

HANDLE WINAPI CreateSemaphore(  _In_opt_  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,  _In_      LONG lInitialCount,      // 初始当前可用资源数目  _In_      LONG lMaximumCount,      // 最大可用资源数目  _In_opt_  LPCTSTR lpName);

CreateSemaphoreEx


获取信号量对象句柄

HANDLE WINAPI OpenSemaphore(  _In_  DWORD dwDesiredAccess,  _In_  BOOL bInheritHandle,  _In_  LPCTSTR lpName);

递增信号量可用资源

BOOL WINAPI ReleaseSemaphore(  _In_       HANDLE hSemaphore,  _In_       LONG lReleaseCount,  _Out_opt_  LPLONG lpPreviousCount);

互斥量内核对象

互斥量内核对象用于确保资源被唯一的线程访问,即互斥访问。

创建(获取)互斥量对象

HANDLE WINAPI CreateMutex(  _In_opt_  LPSECURITY_ATTRIBUTES lpMutexAttributes,  _In_      BOOL bInitialOwner,  _In_opt_  LPCTSTR lpName);

互斥量内核对象 有 引用计数器,线程ID已经递归计数器组成

线程ID用来记录当前获取互斥量对象的线程ID,0表示没人获取,互斥量处于触发状态。一旦,有个线程wait到互斥量,其内核对象线程ID为该线程ID,同时内核对象变为未触发状态,其他线程只能继续等待。但对于已经获得互斥量线程,其仍可以等待成功,这时候内核对象会递增其递归计数器。

调用ReleaseMutex 释放互斥量。对于多次递归进入的互斥量,要相应的多次调用release函数。

注意,当线程在获取了互斥量对象,而在调用ReleaseMutex之前结束的话,会产生遗弃问题


关于内核态同步对象的一些事项

1、一般的,通过内核对象来进行同步,其获取的内核对象句柄都是具有所有权限的(访问,改变触发状态等),但是我们可以在Create内核对象时,通过扩展函数ex函数设置可访问权限,那么当访问该内核对象句柄时,仅能够进行指定的权限访问。

2、内核对象命名与多用户系统

我们有多种方法可以在多个进程空间访问同一个内核对象(继承,dumplicatehandle,命名的内核对象)。

在使用命名内核对象访问时,需要注意在多用户系统中内核对象名称的前缀。

MSDN原话是:

The name can have a "Global\" or "Local\" prefix to explicitly create the object in the global or session namespace.

即加上“Global\”前缀,可以在多个用户间通过名称访问该内核对象,而"Local\"前缀仅能够当前用户通过名称访问内核对象。



0 0