Windows核心编程笔记(6)----用户模式下的线程同步

来源:互联网 发布:清梦繁华冢 知乎 编辑:程序博客网 时间:2024/05/15 23:50

1、原子锁

使用InterlockedExchangeAdd函数来实现原子增长,InterlockedExchange\InterlockedExchangePointer用来交换两个变
量的值,InterlockedCompareExchange对比数值,相等则交换(对应的InterlockedCompareExchangePointer)。对应的
还有64位函数。
InterlockedIncrement\InterlockedDecrement是比较老的函数,只能增加或递减1,InterlockedExchangeAdd的灵活性更
大。

2、Interlocked 单向链表操作函数(支持原子操作的链表)

InitializeSListHead创建一个空栈
InterlockedPushEntrySList入栈
InterlockedPopEntrySList出栈
InterlockedFlushSList清空栈
QueryDepthSList获取栈元素个数

3、高速缓存行

CPU从内存中取出指令时,一次取出高速缓存行大小个字节(32、64、128因CPU型号而异),CPU就不用访问内存总线,直
接从缓存中读取指令比内存中读取快多了。
由于多个CPU取出同一块内存数据到各自的高速缓存行,导致内存数据不一致。CPU设计时,当一个CPU修改了它的告诉缓
存行后,其他CPU会收到通知,并使自己的高速缓存行作废。
这意味着,我们应该根据高速缓存行的大小来讲应用程序的数据组织在一起,并将数据与缓存行的边界对齐。这样做的目
的是为了确保不同的CPU能各自访问不同的内存地址,而且这些地址不在同一个高速缓冲行内。

4、使用volatile关键字

volatile告诉编译器,不要对这个变量进行任何形式的优化,而是始终从变量所在内存中的位置读取变量的值。
编译器的优化,编译器读取数据到CPU寄存器中,下次需要该数值时直接取寄存器中的值,这样即使变量数值已经改变也
无法知道。使用volatile后,CPU每次都去变量所在内存读取,变量改变后可以及时获取。

5、使用关键段(临界区)CRITICALSECTION

EnterCriticalSection\LeaveCriticalSection,使用前需要初始化InitializeCriticalSection,使用后需要释放
DeleteCriticalSection。
关键段优点:易使用,执行速度快;缺点:无法用在多个进程之间对线程进行同步。
同时有两个线程访问资源时,EnterCriticalSection会使一个获得资源,另一个切换到等待状态。如果等待时间太长,
最终回引发异常,超时时间保存在注册表:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\
Session Manager\CriticalSectionTimeout中。默认大约是30天。
可以使用TryEnterCriticalSection来替代EnterCriticalSection,TryEnterCriticalSection会立即返回资源是否正在
被占用。

6、关键段和旋转锁

当一个线程试图进入一个关键段,而这个关键段正在被另一个线程占用时,函数会立即把调用线程切换到等待状态。这
意味着线程必须从用户模式切换到内核模式(大约1000个CPU周期),这个开销十分大。为了提高关键段的性能,
Microsoft把旋转锁合并到了关键段中。调用EnterCriticalSection时,会调用一个旋转锁不断循环,尝试获得资源的
访问权。只有在尝试失败后,才会切换进内核状态。为了使用带旋转锁的关键段,需要使用API 
InitializeCriticalSectionAndSpinCount来初始化关键段,第二个参数制定循环的次数,如果是在单CPU机器上这个
参数将会被忽略。
SetCriticalSectionSpinCount用来改变旋转锁循环次数。用来保护进程堆的关键段所使用的循环次数大约是4000,这可以作为我们的一个参考值。
EnterCriticalSection在内存不足时会导致异常,但是无返回值;InitializeCriticalSectionAndSpinCount在内存不足时,返回值为FALSE,便于直接测试是否创建成功。

7、Slim读写锁

在Vista以上本版才有此函数,忽略。

8、以上几种线程同步方式的性能:

如果希望应用程序得到最佳性能,首先尝试不要共享数据,然后依次使用volatile读取、volatile写入,Interlocked
API,SRWLock以及关键段。当前仅当这些都不能满足要求时,再使用内核对象(每次都需要在用户模式和内核模式之间
切换,CPU开销十分大)。

9、使用技巧

(1)以原子方式操作一组对象时使用一个锁,缺点是降低了可伸缩性:任何时刻系统只允许一个线程运行。
(2)同时访问多个资源时,每个资源都有自己的锁。在获取资源时,所有线程必须以相同顺序来执行。

(3)不要长时间占用锁,其他线程可能进入等待状态,影响程序性能


以下是测试代码:

<span style="white-space:pre"></span>//获取CPU核心数以及每个CPU的高速缓存行的大小
<span style="white-space:pre"></span>PSYSTEM_LOGICAL_PROCESSOR_INFORMATION buffer = NULL;DWORD dwLen = 0;while( true ){if ( GetLogicalProcessorInformation(buffer, &dwLen) )break;if ( GetLastError() != ERROR_INSUFFICIENT_BUFFER ){cout<<"Error code = "<<GetLastError()<<endl;return 1;}if ( buffer )free(buffer);buffer = (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION)malloc(dwLen);if ( NULL == buffer ){cout<<"Error to malloc"<<endl;return 2;}}int nProcCoreCount= 0;int nByteOffset= 0;PSYSTEM_LOGICAL_PROCESSOR_INFORMATION ptr = buffer;while( nByteOffset<dwLen ){switch( ptr->Relationship ){case RelationProcessorCore:nProcCoreCount++;break;case RelationCache:cout<<"cpu"<<nProcCoreCount<<"'s cache size is "<<ptr->Cache.LineSize<<"byte"<<endl;break;default:break;}nByteOffset += sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION);ptr++;}cout<<"ProcessorCore count is "<<nProcCoreCount<<endl;free(buffer);
/*////////////////////////////////////////////旋转锁CPU不断比较两个值,会消耗CPU时间。这里假定所有线程都以相同的优先级运行,对于需要用旋转锁的线程,可能需要使用SetProcessPriorityBoost或者SetThreadPriorityBoost函数来禁止线程优先级提升。在只有单处理器上的机器不应该使用旋转锁,否则容易造成死锁。*/DWORDWINAPI Thread1(LPVOID lpParam){while( InterlockedExchange(&g_bUse, TRUE) == TRUE ){//返回TRUE表示正在被使用,继续等待Sleep(0);}//返回FALSE,表示当前没有被使用,我们已经将其设置为正在被使用//do somethingg_nIndex++;Sleep(300);//使用完了后,设置状态为未使用InterlockedExchange(&g_bUse, FALSE);return 0;}




1 0
原创粉丝点击