当所有线程都能够独立运行而不需要相互通信的时候,Microsoft Windows将进入最佳运行状态。但是,很少有线程能够总是独立运行,通常创建线程是为了处理某些任务,当任务完成时,另一个线程将要得到通知。这就将会照成资源竞争的状况。
在下面两种情况,线程需要线程同步。
1. 需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性。
2. 一个线程需要通知其他线程某项任务完成时
线程同步方式:
原子访问
线程同步的一大部分与原子访问有关。所谓原子访问,指的是一个线程在访问某个资源的同时能够保证没有其他线程会在同一时刻访问同一资源。
Long g_x = 0;
DWORD WINAPI ThreadFunc1(PVOID pvParam) { g_x++; return 0}
假设转换的汇编伪代码
MOV EAX, [g_x]
INC EAX
MOV [g_x], EAX
当有两个线程调用这个函数时,如果按照调用先后顺序来,g_x的值是2,但是实际上windows是抢占式操作系统,这个值只是有可能是2,假设当第一个线程和第一个线程同时调用MOV EAX,[g_x]或者第一个线程或第二线程调用后马上切换另一个线程,那么最后g_x的值就是1而不是2.
为了解决上诉问题,我们只需要保证递加操作用原子操作——也就是说递加的时候,不会被打断。下述函数都是使用原子操作。
LONG InterlockedExchangeAdd(PLONG volatile plAddend, LONG lIncrement);
32位:向plAddend值中递加lIncrement
LONGLONG InterlockedExchangeAdd64(PLONGLONG volatile pllAddend, LONGLONG llIncrement);
64位:向plAddend值中递加lIncrement
LONG InterlockedExchange(PLONG volatile plTarget, LONG lValue);
32位:修改plTarget的值为lValue
LONG InterlockedExchange64(PLONGLONG volatile plTarget, LONGLONG lValue);
64位:修改plTarget的值为lValue
LONG InterlockedExchangePointer(PVOID volatile ppvTarget, PVOID pvValue);
修改ppvTarget的值为pvValue,不限类型,前提是允许=操作。
PLONG InterlockedCompareExchange(PLONG plDestination, LONG lExchange, LONG lComparand);
先将plDestination与lComparand相比,如果相等,则将plDestination赋为lExchange,否则不变。
PLONG InterlockedCompareExchangePointer(PVOID* plDestination, PVOID lExchange, PVOID lComparand);
先将plDestination与lComparand相比,如果相等,则将plDestination赋为lExchange,否则不变。不限类型,前提是允许==比较操作和=操作。
LONG InterlockedIncrement(PLONG plAddend);
将plAddend递加1
LONG InterlockedDecrement(PLONG plAddend);
将plAddend递减1
旋转锁
旋转锁是指在while循环中检查条件是否满足,不满足然后睡眠,醒来过后接着进入循环判断。
高速缓存行
如果想为配有多处理器的机器构建高性能应用程序,那么应该注意高速缓存行。当CPU从内存获取一个字节时,它并不只是从内存取回一个字节,而是取回一个高速缓存行。包含字节取决于CPU。高速缓存行存在的主要目的是提高性能,CPU不必每次都访问内存总线,但是高速缓存线使得内存的更新变得更加困难,
1. CPU1读取一个字节,使得该字节以及它相邻的字节被读到CPU1的高速缓存行
2. CPU2读取同一个字节,使得该字节被读到CPU2的高速缓存行。
3. CPU1对内存中这个字节进行修改,使得该字节被写入到CPU1的高速缓存行,但这信息并没有写会内存
4. CPU2再次读取同一字节。由于该字节已经在CPU2的高速缓存行中,因此CPU2不需要再访问内存。但CPU2将无法看到该字节在内存中新的值。
这种设计非常糟糕。当然,CPU芯片的设计者非常清楚这个问题,并做了专门的设计来进行处理(即当一个CPU的高速缓存行中的一个字节被修改时,机器中的其他CPU会收到通知,并使自己的高速缓存行作废)。
由上可以看出,我们应该根据高速缓存行的大小来将应用程序的数据组织在一起,并将数据与缓存行的边界对齐。这样做的目的是为了确保不同的CPU能够各自访问不同的内存地址,而且这些地址不在同一个缓冲行中。
要确定CPU高速缓存行的大小,最简单方法是调用win32的GetLogicalProcessorInformation函数。有了高速缓冲行信息,我们可以使用C/C++编译器的__declspec(align(#))指示符来对字段加以控制。
Volatile限定符告诉编译器这个变量可能会被应用程序之外的其他东西修改。确切的说,valatile限定符告诉编译器不要对这个变量进行任何形式的优化,而是始终从变量在内存中的位置读取变量的值。
关键段
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);
初始化CRITICAL_SECTION结构
VOID DeleteCriticalSection(PCRITICAL_SECTION pcs);
清理CRITICAL_SECTION结构
VOID EnterCriticalSection(PCRITICAL_SECTION pcs);
检查CRITICAL_SECTION结构中的成员变量,这些变量表示是否有线程正在访问资源以及那个线程正在访问资源。
如果没有线程正在访问资源,那么函数会更新成员变量,以表示调用线程已经获准对资源的访问,并立即返回。
如果成员变量已经获准访问资源,那么函数会更新变量,以表示调用线程被获准访问的次数,并立即返回。
如果成员变量表示有一个除调用线程之外的线程已经获准访问资源,那么函数会使用一个事件内核对象来把调用线程换到等待状态。
VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);
函数会检查结构内部的成员变量并将计数器减1,该计数器用来表示调用线程获准访问共享资源的次数。如果计数器大于0,将直接返回。如果计数器变成了0,函数会更新成员变量,以表示没有任何线程正在访问被保护的资源。
BOOL InitializeCriticalSectionAndSpinCount(PCRITICAL_SECTION pcs, DWORD dwSpin);
函数与InitializeCriticalSection相似,函数在检查到被保护的资源正在被其他线程所使用时,不会马上切换到内核模式,而是在用户模式下循环dwSpin次(即采用旋转锁)
BOOL SetCriticalSectionSpinCount(PCRITICAL_SECTION pcs, DWORD dwSpin);
设置旋转锁的循环次数
BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs);
不会让线程进入等待状态。通过返回值来表示线程是否获准访问资源。
关键段的错误处理
当我们初始化CRITICAL_SECTION结构后,调用EnterCriticalSection函数发生竞争资源的时候会创建一个事件内核对象,当内存不足时,初始化函数会抛出异常EXCEPTION_INVALID_HANDLE,一种解决方法是用结构化异常处理来捕获错误,另外一种方法是用InitializeCriticalSectionAndSpinCount来创建关键段,并将dwSpin参数的最高位设为1(代表最初的时候就创建一个与关键段想关联的事件内核对象)。但是这样做很浪费系统资源,只要在满足3钟情况下才应该这样做,1.我们不能接受调用EnterCriticalSection失败2.我们知道争夺现象一定会发生3.我们预计进程会在内存不足的环境下运行。
Slim读/写锁
SRWLock的目的和关键段相同:对一个资源进行保护,不让其他线程访问它。与关键段不同的是,SRWLock允许我们区分哪些想要读取资源的值的线程和想进行更新资源的值的线程。让所有的读取者线程能在同一时刻访问共享资源,而控制写入者线程只能独占资源。
VOID InitializeSRWLock(PSRWLOCK SRWLock);
初始化SRWLOCK结构
VOID AcquireSRWLockExclusive(PSRWLOCK SRWLock);
写入者线程,传入SRWLOCK结构的地址,获取资源的独占访问权
VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
写入者线程,解除资源的锁定
VOID AcquireSRWLockShared(PSRWLOCK SRWLock);
读取者线程,获取资源的共享访问权。
VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);
读取者,解除资源的锁定
与关键段相比,SRWLock缺乏下面两个特性:
1. 不存在TryEnter(Shared/Exclusive)SRWLock之内的函数:如果锁已经被占用,那么调用AcquireSRWLock(Shared/Exclusive)会阻塞调用线程。
2. 不能递归的获得SRWLOCK。也就是说,一个线程不能为了多次写入资源而多次锁定资源,然后再多次调用ReleaseSRWLock*来释放对资源的锁定。
双处理器采用同步机制来运行同样任务,记录时间。(不同机器不同值)
同步机制性能比较
线程/ 微妙 | Volatile 读取 | Volatile 写入 | Interlocked 递增 | 关键段 | SRWLock 共享模式 | SRWLock 独占模式 | 互斥量 |
1 | 8 | 8 | 35 | 66 | 66 | 67 | 1060 |
2 | 8 | 76 | 153 | 268 | 134 | 148 | 11082 |
4 | 9 | 145 | 361 | 768 | 244 | 307 | 23785 |
让线程以原子方式把锁释放并将自己阻塞,直到某一个条件成立为止。
BOOL SleepConditionVariableCS(PCONDITION_VARIABLE pConditionVariable
PCRITICAL_SECTION pCriticalSection,
DWORD dwMilliseconds);
函数首先释放pCriticalSection锁,然后调用线程等待条件变量pConditionVariable成立,等待dwMilliseconds毫秒,线程将自己阻塞,pCriticalSection同步对资源的访问。当指定时间用完时,如果条件变量未被触发,函数返回FALSE,否则返回TRUE.返回TRUE时,函数已经获得锁。FALSE时未获得锁。
BOOL SleepConditionVariableSRW(PCONDITION_VARIABLE pConditionVariable
PSRWLOCK pSRWLock,
DWORD dwMilliseconds,
ULONG Flags);
函数首先释放pCriticalSection锁,然后调用线程等待条件变量pConditionVariable成立,等待dwMilliseconds毫秒,线程将自己阻塞,pCriticalSection同步对资源的访问,Flags表示条件成立后获得的权限(写入/读取)。当指定时间用完时,如果条件变量未被触发,函数返回FALSE,否则返回TRUE.返回TRUE时,函数已经获得锁且已获得某一种权限。FALSE时未获得锁。