第八章:用户模式下的线程同步

时间:2022-12-09 23:33:20
1:Interlocked系列函数

1.1:Interlocked系列函数都提供64位版本,以下文字不将64位版本函数,具体请参考书上

1.2:Interlocked系列函数在x86平台下工作原理是在总线上维持一个硬件信号,这个信号会阻止其他CPU访问同一个内存地址

1.3:调用一个Interlocked系列函数只会暂用50个以下CPU周期,而进行一次用户态内核态模式切换一般在1000个CPU周期以上

确保锁变量和锁所保护的数据在不同的高速缓存行,不然会损伤性能,下面是我的理解:

CONDITION_VARIABLE cv;
InitializeConditionVariable(&cv);

SleepConditionVariableCS(&cv,pCS,INFINITY);
SleepConditionVariableSRW(&cv,pSRW,INFINITY,0或者Share);//0表示独占方式,Share共享方式

WakeConditionVariable(&cv);
WakeAllConditionVariable(&cv);

//CONDITION_VARIABLE不需要Delete,因为操作系统底层不使用它


一个CPU获得锁,从而修改锁所保护的数据,这会引起其他在循环等待此锁变量CPU的高速缓存行失效,因为锁变量和锁保护的数据在一起,这些CPU又会去读取锁变量,一直这样循环

另一个猜测是:如1.4.2所示的循环锁一样,如果g_bUse为true,为其赋值g_bUse=true不会引起其他引用了此变量的高速缓存行失效.

1.4:Interlocked系列函数包括以下几个

1.4.1:InterlockedExchangeAdd(&g_x,-1);InterlockedIncrement(&g_x);InterlockedDecrement(g_x);

1.4.2:InterlockedExchange(&g_bUse,true);使用这个函数创建一个简单的循环锁例子:

while(InterlockedExchange(&g_bUse,true)==true)
Sleep(0);

此函数会设置g_bUse为true,并返回之前的g_bUse值

使用这段代码的线程应该关闭线程动态优先级提升,使所有线程都处在同一优先级水平,否则会影响性能

旋转锁在单CPU环境下意义不大,最好旋转次数经验值为4000次

1.4.3:InterlockCompareExchange(PLONG,LONG,LONG);

这个函数将参数1和参数3比较,如果相同,则将参数2赋值给参数1,所有操作都是原子的

1.5:Interlocked系列函数还定义了一些被称为Interlocked单向链表栈的函数,具体参考书上

2:高速缓存行

2.1:当CPU从内存读取一个字节数据时,并不只是取回一个字节,而是取回一个高速缓存行,取决于CPU,这个高速缓存行大小为32,64,128字节

2.2:当CPU1修改自己高速缓存行的一个字节时,会通知其他CPU,这会使其他CPU高速缓存行作废,CPU1会将自己的高速缓存行写回内存

2.3:GetLogicalProcessorInformation()可以获得CPU高速缓存行大小,但是我搞了半天得不到,好像Windows没实现这个函数,坑爹啊,但x86高速缓存行是32字节,IA64是64字节

2.4:用如下方法控制数据在不同的高速缓存行

#define CACHE_ALIGN 32
struct _declspec(align(CACHE_ALIGN)) stElement
{
int a;
int b;//a,b在一个高速缓存行中
_declspec(align(CACHE_ALIGN))
int c;
int d;//c,d在另一个高速缓存行中
};

2.5:最好的避免高速缓存行引起性能损失的方式是:使用函数参数和局部变量,这些数据存在于线程栈中,不会引起上述问题,还可以使用线程关联性始终只让一个CPU访问一块数据,这也不会引起上述问题

备注:关于高速缓存行我的理解

每个CPU都有一个Cache,每个Cache由多个高速缓存行组成,每个高速缓存行大小应该等于数据总线宽度,可以把在Cache中的高速缓存行看成一个数组,数组的索引是内存地址的一部分,当从内存中读取数据时,数据的内存地址被计算出索引,然后存储到相应的高速缓存行中,CPU只和高速缓存行通信,高速缓存行和内存通信,每个Cache都有个控制器,用于监视所有CPU所有高速缓存行状态,并使用MESI等协议维护其一致性,当一个CPU修改其高速缓存行中的数据时,根据当前高速缓存行状态,会通知其他CPU高速缓存行,使其无效或者不受影响

3:Volatile

Volatile告诉CPU从内存中读取这个变量而不是使用寄存器中的值

4:关键代码段

4.1:基本函数如下:

CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
EnterCriticalSection(&cs);
LeaveCriticalSection(&cs);
DeleteCriticalSection(&cs);

关键代码段内部也使用了Interlock函数,所以速度非常快

EnterCriticalSection即使在多CPU下也是安全的

EnterCriticalSection()会增加CRITICAL_SECTION计数,所以对应的应该多次调用LeaveCriticalSection()

EnterCriticalSection()如果检测到已经有已经有线程Enter,则会创建一个事件内核对象,挂起当前线程,当然永远只会创建一个事件内核对象,并在DeleteCriticalSection()中析构

被EnterCriticalSection()挂起的线程会超时,默认为30天,这你妹的太长啦

4.2:可以使用如下函数代替EnterCriticalSection();

BOOL TryEnterCriticalSection(&cs);

这个函数不会挂起调用线程,会通过返回值决定是否进入了关键代码段,如果返回true,代码执行完成后应该调用LeaveCriticalSection()递减计数

4.3:LeaveCriticalSection()被调用后,会更新CRITICAL_SECTION结构体,并且会检查有没有其他线程在等待此关键代码段,如果有,则切换其中一个线程为可调度状态

4.4:如果线程阻塞在EnterCriticalSection(),会挂起自己,并进入内核模式,这会使用1000个CPU周期,可以使用如下函数旋转等待

BOOL InitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION lpcs,DWORD dwSpinCount);
DWORD SetCriticalSectionSpinCount(LPCRITICAL_SECTION lpcs,DWORD dwSpinCount);

但单CPU下会忽略旋转次数,因为这没有意义

4000是经验值最佳旋转次数

如果给InitializeCriticalSectionAndSpinCount中的dwSpinCount最高位传递1,则会预先把事件内核对象创建好

5:SRWLock
5.1:XP没有SRWLock,使用方式:

RTL_SRWLOCK sl;
InitializeSRWLock(&sl);
AcquireSRWLockExclusive(&sl);//独占锁(写锁)
ReleaseSRWLockExclusive(&sl);
AcquireSRWLockShared(&sl); //共享锁(读锁)
ReleaseSRWLockShared(&sl);

RTL_SRWLOCK无需Delete,右操作系统回收

5.2:为了提高性能,SRWLock没有计数,也就是说Acquire多次只需要Release一次

5.3:多CPU多线程下性能比较:

Volatile读取由于不上锁,所以一直很快

Volatile写入当CPU增加时,一次写入会引起其他CPU高速缓存行失效,这会影响性能

Interlocked函数会锁定总线,并且如果改变了值,还有引起高速缓存行失效

SRWLock共享锁性能很好,因为只读,不会引起高速缓存行失效,并且也不需要同步

SRWLock独占锁会引起高速缓存行失效,并且需要同步,所以速度较慢

整体来说:Volatile>Interlocked>SRWLock>关键段>通过内核对象的同步

6:条件变量

6.1:条件变量可以完成的功能:

以原子方式把一个线程的锁释放并把自己阻塞,直到一个条件成立为止,当条件成立时,获得锁

线程可以设置条件变量,这样可以唤醒一个或多个线程

6.3:相关函数

CONDITION_VARIABLE cv;
InitializeConditionVariable(&cv);

SleepConditionVariableCS(&cv,pCS,INFINITY);
SleepConditionVariableSRW(&cv,pSRW,INFINITY,0或者Share);//0表示独占方式,Share共享方式

WakeConditionVariable(&cv);
WakeAllConditionVariable(&cv);

//CONDITION_VARIABLE不需要Delete,因为操作系统底层不使用它

6.2:生产者消费者问题:

生产者生产物品,将物品放入链表,消费者消费链表中的物品,生产者需保证当链表满时把自己阻塞,这样不会浪费CPU时间并且不会占用锁,给更多机会给消费者;消费者同样必须保证当链表空时把自己阻塞

老式方法:

//生产者 
unsigned _stdcall ProducerFunc(void *pParam)
{
for (unsigned int i = 0;i < g_uiIterations;i++)
{
EnterCriticalSection(&g_csLock);
// Produce work
g_listWork.push_back(i++);
LeaveCriticalSection(&g_csLock);
SetEvent(g_hNotEmpty);//使用自动重置和手动重置会有不同的特性
Sleep(g_uiProducerDelay); // Simulating work
}
return 0;
}
//消费者
while (true)
{
EnterCriticalSection(&g_csLock);

if (g_listWork.empty())
{
LeaveCriticalSection(&g_csLock);
WaitForSingleObject(g_hNotEmpty,INFINITE);
}
else
{
i = g_listWork.front();
g_listWork.pop_front();

LeaveCriticalSection(&g_csLock);

wcout << L"Thread " << iThread << L" Consumed: " << i << endl;
Sleep(g_uiConsumerDelay); // Simulating work
}
}

缺点:代码复杂,如果是手动重置,还必须小心调用ResetEvent();

使用条件变量:

生产者:

for (unsigned int i = 0;i < g_uiIterations;i++)
{
EnterCriticalSection(&g_csLock);

// Produce work
g_listWork.push_back(i);

LeaveCriticalSection(&g_csLock);

WakeConditionVariable(&g_condNotEmpty);

Sleep(g_uiProducerDelay);
}

消费者:

while (true)
{
EnterCriticalSection(&g_csLock);

while (g_listWork.empty())
{
SleepConditionVariableCS(&g_condNotEmpty,&g_csLock,INFINITE);
}

i = g_listWork.front();
g_listWork.pop_front();

LeaveCriticalSection(&g_csLock);

wcout << L"Thread " << iThread << L" Consumed: " << i << endl;
Sleep(g_uiConsumerDelay); // Simulating work
}

8:一次性初始化
8.1:为什么线程安全的单件DCL是不安全的:

这是因为 C++ 标准没有定义线程模型。它假定只有一个执行线程,并且没有定义可供开发人员表示相关指令排序的约束的方式,这使得编译器可以*重新排序内存的读取和写入。在多线程环境中,重新排序可能会导致线程在实际执行源代码中位置比它更靠前的所有语句之前就观察内存的写入。
8.2:同步(阻塞)一次性初始化相关函数:

//初始化INIT_ONCE结构
VOID WINAPI InitOnceInitialize(PINIT_ONCE InitOnce);

//初始化INIT_ONCE结构并设置初始化回调函数InitFn,传入回调函数的参数Parameter,和一个返回的指针Context
BOOL WINAPI InitOnceExecuteOnce(PINIT_ONCE InitOnce,
PINIT_ONCE_FN InitFn,
PVOID Parameter,
LPVOID* Context);

//回调函数,初始化成功后,将指针放入Context中
BOOL CALLBACK InitOnceCallback(PINIT_ONCE InitOnce,
PVOID Parameter,
PVOID* Context);

8.3:同步(阻塞)一次性初始化示例:

BOOL WINAPI InitLoggerFunction(PINIT_ONCE intOncePtr,
PVOID Parameter,
PVOID* contextPtr)
{
try
{
Logger* pLogger = new Logger();
*contextPtr = pLogger;
return TRUE;
}
catch (...)
{
// Something went wrong.
return FALSE;
}
}

Logger* GetLogger()
{
static INIT_ONCE initOnce;

PVOID contextPtr;
BOOL status;

status = InitOnceExecuteOnce(&initOnce,
InitLoggerFunction,
NULL,
&contextPtr);

if (status)
{
return (Logger*)contextPtr;
}

return NULL;
}

8.4:异步(非阻塞)一次性初始化相关函数:

BOOL WINAPI InitOnceBeginInitialize(
LPINIT_ONCE InitOnce,
DWORD dwFlags,
PBOOL fPending,
LPVOID* Context
);

BOOL WINAPI InitOnceComplete(
LPINIT_ONCE lpInitOnce,
DWORD dwFlags,
LPVOID lpContext
);

8.5:异步(非阻塞)一次性初始化示例:

Logger* GetLogger()
{
static INIT_ONCE initOnce;

PVOID contextPtr;
BOOL fStatus;
BOOL fPending;

fStatus = InitOnceBeginInitialize(&initOnce,INIT_ONCE_ASYNC,
&fPending, &contextPtr);

// Failed?
if (!fStatus)
{
return NULL;
}

// Initialization already completed?
if (!fPending)
{
// Pointer to the logger is contained context
pointer.
return (Logger*)contextPtr;
}

// Reaching this point means that the logger needs to be created.

try
{
Logger* pLogger = new Logger();

fStatus = InitOnceComplete(
&initOnce,INIT_ONCE_ASYNC,(PVOID)pLogger);

if (fStatus)
{
// The Logger that was created was successfully
// set as the logger instance to be used.
return pLogger;
}

// Reaching this point means fStatus is FALSE and the object this
// thread created is not the sole instance.
// Clean up the object this thread created.
delete pLogger;
}
catch (...)
{
// Instantiating the logger failed.
// Fall through and see if any of
// the other threads were successful.
}

// Call again but this time ask only for
// the result of one-time initialization.
fStatus = InitOnceBeginInitialize(&initOnce,INIT_ONCE_CHECK_ONLY,
&fPending,contextPtr);

// Function succeeded and initialization is complete.
if (fStatus)
{
// The pointer to the logger is in the contextPtr.
return (Logger*) contextPtr;
}

// InitOnceBeginInitialize failed, return NULL.
return NULL;
}

8.6:一个特性

在同步初始化中,如果初始化不成功,则会再次调用回调函数,知道初始化成功,如果程序希望这样一种策略:初始化一次,如果不成功就返回则要做些特别的事情,有以下两个方法实现此功能

方法1:如果初始化不成功,也让回调函数返回TRUE,但在lpContext 设置一个值通知此初始化不成功,我们应该去检验返回的lpContext 判断是否初始化成功

方法2:使用InitiOnceBeginInitialize/InitOnceComplete API 代替 InitOnceExecuteOnce API 来进行同步初始化

在dwFlags不使用INIT_ONCE_ASYNC 标志,将代码置于InitiOnceBeginInitialize/InitOnceComplete之间

此方法具体使用请参考更多文章,这里不深入了

 7:一个原则

对一段代码加多个锁,保证在代码的任何地方,以相同的顺序获得锁