线程同步-用户模式下同步(原子锁、临界区、读写锁)

来源:互联网 发布:飞机大战java代码框架 编辑:程序博客网 时间:2024/05/19 01:13

一、线程同步

当所有的线程都能够独自运行而不需要相互通信的时候,Windows将进入最佳运行状态。但是,很少有线程能够总是独自运行。通常创建线程是为了处理某些任务,当任务完成的时候,另一个线程可能想要得到通知。 系统中所有的线程必须访问系统资源,比如堆、串口、文件、窗口以及无数其他资源,如果一个线程独占了对某个资源的访问,那么其他线程都无法对某个资源的访问。

  • 线程需要相互通信的情况
    • 需要让duoge线程同时访问一个共享资源,同时不能破坏资源的完整性;
    • 一个线程需要通知其他线程某项任务已经完成。

二、用户模式下的线程同步

1. 原子访问

// 老版本  一次只能加减1LONG InterlockedIncrement(PLONG plAddend);LONG InterlockedDecrement(PLONG plAddend);LONG InterlockedExchangeAdd(    PLONG   volatile plAddend,       //原子操作的数    LONG    lIncrement);            // 加的值LONGLONG InterlockedExchangeAdd64(    PLONGLONG volatile pllAddend,   //原子操作的数    对应是LONGLONG    LONGLONG llIncrement);          // 加的值
LONG __cdecl InterlockedExchange(     // 操作32位数,    _Inout_ LONG volatile *Target,    _In_    LONG          Value    );PVOID __cdecl InterlockedExchangePointer( //在32位机器上操作32位数,在64位机器上操作64位数  _Inout_ PVOID volatile *Target,  _In_    PVOID          Value);LONGLONG __cdecl InterlockedExchange64(  //操作64位数  _Inout_ LONGLONG volatile *Target,  _In_    LONGLONG          Value);

以上3个函数都将以原子操作交换两个数,并返回第一个参数原来的值。

旋转锁(spinlock):

//BOOL g_fResourceInUser = FALSE;void Func1() {    while (InterlockedExchange (&g_fResourceInUser, TRUE) == TRUE)        Sleep(0);    ...    InterlockedExchange(&g_fResourceInUser, FALSE);}

while循环中把 g_fResourceInUser 的值设为TRUE并检查原来的值是否为TRUE。如果原来的值为FALSE,那说明资源尚未使用,线程就立刻将其设为”使用中”,然后退出循环。如果原来的值是TRUE,则说明正在使用中,while循环继续执行。

在单CPU计算机上需避免使用旋转锁,如果一个线程不停地循环,那么这不仅会浪费CPU时间,也会阻止其他线程改变锁的值。可以每次增加Sleep的时间,以优化旋转锁,也可以将Sleep替换为 SwitchToThread。

PLONG InterlockedCompareExchange(    PLONG   plDestination,    LONG    lExchange,    LONG    lComparand);PLONG InterlockedCompareExchangePointer(    PVOID*  ppvDestination,    PVOID   pvExchange,    PVOID   pvComparand);LONGLONG InterlockedCompareExchange64(    LONGLONG pllDestination,    LONGLONG llExchange,    LONGLONG llComparand);//伪代码LONG InterlockedCompareExchange(PLONG plDestination,    LONG lExchange, LONG lComparand) {    LONG lRet = *plDestination;    if (*plDestination == lComparand)        * plDestination = lExchange;    return lRet;}

将第一个参数与第三个参数对比,如果相等则替换成第二个参数,并返回第一个参数原来的值

2.临界区

在有一个线程进入临界区后,其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的,临界区的API原型如下所示:

/*初始化临界区*/void InitializeCriticalSection(LCPRITICAL_SECTION    lpCriticalSection);    //初始化void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection); /*进入临界区*/void EnterCriticalSection(LPCRITICAL_SECTION    lpCriticalSection);    //进入BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs);void LeaveCriticalSection(LPCRITICAL_SECTION    lpCriticalSection);    //离开

EnterCriticalSection 函数进入一个临界区时,若临界区是不可进入状态,会导致线程一直处于挂起等待状态,可以使用 TryEnterCriticalSection替代,该API不会让调用线程进入等待状态,它会通过返回值来表示调用线程是否获准访问资源。如果不能访问,返回FALSE,线程可进行其他事情,如果返回TRUE,CRITICAL_SECTION的成员变量已经更新过了,表示该线程正在访问资源,必须调用一个对应的 LeaveCriticalSection。

当线程试图进入一个关键段,但此关键段正被另一个线程占用的时候,函数会立即把调用线程切换到等待状态,
这意味着线程必须从用户模式切换到内核模式(大约1000个CPU周期),这个切换的开销非常大。而配有多处理器的机器上,当前占用的线程可能在另一个处理器上运行,而且可能很快就会结束对资源的访问,事实上,在需要等待的线程完全切换到内核模式之前,占用资源的线程可能就已经释放了资源。为了提高关键段的性能,把旋转锁合并到关键段中。因此在调用EnterCriticalSection的时候,它会用一个旋转锁不断地循环,尝试在一段时间内获得资源的访问权,只有当尝试失败的时候,线程才会切换到内核模式并进入等待状态。

相关API:

// 以旋转锁模式等待初始化一个临界区BOOL InitializeCriticalSectionAndSpinCount(    PCRITICAL_SECTION pcs,  //临界区结构地址    DWORD dwSpinCount);     // 旋转锁循环的次数   一般设置为4000// 设置临界区的旋转锁旋转次数DWORD SetCriticalSectionSpinCount(    PCRITICAL_SECTION  pcs,    DWORD dwSpinCount);
  • 临界区使用异常处理
    • InitializeCriticalSectin 在内部会分配一块内存,以提供一些内部调试信息,如果内存分配失败,函数会抛出 STATUS_NO_MEMORY 异常;
    • 如果两个或两个以上的线程在同一时刻争夺同一临界区时,临界区会在内部使用一个事件内核对象,这个时间只有在发生争抢时,用到的时候才会创建;
    • 在 Windows XP以前,内存不足的情况下,发生争夺临界区段时,系统可能因为无法创建所需要的事件内核对象,这个时候EnterCriticalSection函数会抛出 EXCEPTION_INVALID_HANDLE 异常。(避免该错误可以使用 InitializeCriticalSectionAndSpinCount来创建关键段,并将dwSpinCount参数的最高位设为1,则初始化临界区段时就会创建事件内核对象)

示例:

int g_nNum = 0;CRITICAL_SECTION g_stcCtlStn    =    {0};DWORD ThreadProc(LPVOID lpParam){    for (int i = 0; i < 5; i++)    {        EnterCriticalSection(&g_stcCtlStn);        printf("%d", g_nNum++);        LeaveCriticalSection(&g_stcCtlStn);    }    return 0;}int _tmain(int argc, _TCHAR* argv[]){    InitializeCriticalSection(&g_stcCtlStn);    CreateThread(NULL, 0, ThreadProc,NULL, 0, nullptr);    CreateThread(NULL, 0, ThreadProc,NULL, 0, nullptr);    system("pause");    return    0;}

3.Slim读/写锁

SRWLock 的目的和临界区相同:对一个资源进行保护,不让其他线程访问它。但是与临界区不同的是,SRWLock允许我们区分那些想要读取资源的值的线程和写入线程。让所有读线程在同一时刻访问共享资源,而写入线程对资源应该是独占的。

分配一个 SRWLOCK 结构并用InitializeSRWLock 函数对它进行初始化:

VOID InitializeSRWLock(PSRWLOCK SRWLock);

写入:

VOID AcquireSRWLockExclusive(PSRWLOCK SRWLock);VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);

读取:

VOID AcquireSRWLockShared(PSRWLOCK SRWLock);VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);

条件变量

BOOL SleepConditionVariableCS(    PCONDITION_VARIABLE pConditionVariable,    PCRITICAL_SECTION   pCriticalSection,    DWORD dwMilliseconds);BOOL SleepConditionVariableSRW(    PCONDITION_VARIABLE pConditionVariable,    PSRWLOCK pSRWLock,    DWORD dwMilliseconds,    ULONG Flags);

pConditionVariable 指向一个已初始化的条件变量,调用线程正在等待该条件变量。第二个参数是指向一个临界区或者SRWLock的指针,该临界区或SRWLock用来同步对共享资源的访问,参数dwMilliseconds 表示等待时间(INFINITE一直等待)。 对于第二个函数的 Flags 参数用来指定一旦条件变量被触发,希望以何种方式来得到锁:对于写入线程,应该传入0,表示希望独占对资源的访问;对读取线程来说,应该传入CONDITION_VARIABLE_LOCKMODE_SHARED,表示希望共享资源的访问。当指定时间用完,如果条件变量尚未被触发,函数会返回FALSE,否则函数会返回TRUE。

用户模式线程同步最大的缺点在于它们无法用来在多个进程之间对线程进行同步。

阅读全文
0 0