首页 > 代码库 > Windows核心编程笔记(6)----用户模式下的线程同步

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

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函数来禁止线程优先级提升。
在只有单处理器上的机器不应该使用旋转锁,否则容易造成死锁。
*/
DWORD	WINAPI Thread1(LPVOID lpParam)
{
	while( InterlockedExchange(&g_bUse, TRUE) == TRUE )
	{//返回TRUE表示正在被使用,继续等待
		Sleep(0);
	}
	//返回FALSE,表示当前没有被使用,我们已经将其设置为正在被使用
	//do something
	g_nIndex++;
	Sleep(300);
	//使用完了后,设置状态为未使用
	InterlockedExchange(&g_bUse, FALSE);
	return 0;
}




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