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;
}
时间: 2024-10-11 08:47:29

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

第8章 用户模式下的线程同步(4)_条件变量(Condition Variable)

8.6 条件变量(Condition Variables)——可利用临界区或SRWLock锁来实现 8.6.1 条件变量的使用 (1)条件变量机制就是为了简化 “生产者-消费者”问题而设计的一种线程同步机制.其目的让线程以原子方式释放锁并将自己阻塞,直到某一个条件成立为止.如读者线程当没有数据可读取时,则应释放锁并等待,直到写者线程产生了新的数据.同理,当写者把数据结构写满时,那么写者应该释放SRWLock并等待,直到读者把数据结构清空. (2)等待函数:SleepConditionVariab

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

8.1 原子访问:Interlocked系列函数(Interlock英文为互锁的意思) (1)原子访问的原理 ①原子访问:指的是一线程在访问某个资源的同时能够保证没有其他线程会在同一时刻访问该资源. ②从汇编的角度看,哪怕很简单的一条高级语言都可以被编译成多条的机器指令.在多线程环境下,这条语句的执行就可能被打断.而在打断期间,其中间结果可能己经被其他线程更改过,从而导致错误的结果. ③在Intelx86指令体系中,有些运算指令加上lock前缀就可以保证该指令操作的原子性.其原理是CPU执行该指

第8章 用户模式下的线程同步(3)_Slim读写锁(SRWLock)

8.5 Slim读/写锁(SRWLock)——轻量级的读写锁 (1)SRWLock锁的目的 ①允许读者线程同一时刻访问共享资源(因为不存在破坏数据的风险) ②写者线程应独占资源的访问权,任何其他线程(含写入的线程)要等这个写者线程访问完才能获得资源. (2)SRWlock锁的使用方法 ①初始化SRWLOCK结构体 InitializeSRWLock(PSRWLOCK pSRWLock); ②写者线程调用AcquireSRWLockExclusive(pSRWLock);以排它方式访问   读者线

用户模式下的线程同步

1 /****************************************************************************** 2 Module: Queue.cpp 3 Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre 4 ******************************************************************************/

Windows核心编程笔记(7)----内核模式下的线程同步

1.内核对象同步与用户模式下同步对比 使用内核对象的唯一缺点就是性能,调用内核对象函数时,调用线程必须从用户模式切换到内核模式,这种切换是相当 耗时的. 内核对象(进程.线程.作业)要么处于触发态,要么处于未触发状态.进程内核对象在创建时总是处于未触发状态, 当进程终止时,操作系统会自动使进程内核对象变成触发状态.当进程内核对象处于触发状态后,将永远保持这种状态, 再也不能变回未触发状态. 2.等待内核对象 WaitForSingleObject等待单个内核对象,WaitForMultipleO

Windows核心编程笔记(1)

最近工作比较闲了,一直没来得及看的核心编程最近开始看了,分享下笔记. 1.内核句柄用完不释放一定会造成内存泄漏吗? 不一定,内核句柄在进程退出时会被系统释放掉(遍历内核句柄表,只要每个句柄指向的内核对象的引用计数为0,内核就会销毁该对象,适用于所有的内核对象.资源(GDI对象在内).内存块): 2.内核对象如何关闭? 调用CloseHandle(),内核会查找该进程的句柄表,如果没找到该句柄,返回FALSE(Debug下抛出异常);如果找到,则使该句柄指向的内核对象引用计数减一,若引用计数为0,

Windows核心编程笔记(5)----线程调度,优先级

1.操作系统线程调度过程 每个线程都有一个上下文CONTEXT结构体,保存在线程的内核对象中,这个上下文中保存了线程上一次执行时CPU寄存器 的状态.每隔固定时间,Windows会查看所有当前存在的线程内核对象,其中只有一些是可调度的.Windows在可调度的 线程中选择一个,并将上次保存到线程上下文中的数据载入CPU寄存器中.(上下文切换) CPU时间片到后,Windows移出这个线程,把CPU寄存器信息保存到线程上下文中,切换到另一个线程,如此循环. 2.线程的挂起和恢复 调用CreateP

Windows核心编程笔记(2)

6 进程实例句柄 6.1 每一个EXE或者DLL被加载到内存中后,都会被赋予一个独一无二的句柄(HINSTANCE),该句柄在WinMain函数调用时传入.获取应用程序相关信息(资源.路径)时,有的需要传入HINSTANC有的需要传入HMODULE,实际上HINSTANC与HMODULE完全是一回事,这是16位Windows系统上不同数据类型造成的. WinMain函数的第一个参数:实例句柄是如何传递进来的呢?查看crtexe.c源码,我们会看到如下代码 #ifdef WPRFLAG mainr

Windows核心编程笔记(3)--作业

/*1.如果进程已经与一个作业相关联,就无法将当前进程及其任何子进程从作业中移除,这个安全特性可以保证 /* 进程无法摆脱对它施加的限制. /*2.在调试程序时,调试器是从资源管理器启动的,程序会从调试器继承带"PCA"前缀的作业.因此,调试程序 /* 时总是显示进程已经加入了作业.使用命令行来运行程序时就不会有这个问题了. /*3.关闭一个作业对象,并不会终止作业内所有的进程.作业对象实际上只是加了一个删除标记,只有在作业中 /* 所有进程都终止后,才会自动销毁. /*4.可以向作业