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

Drecik学习经验分享

转载请注明出处:http://blog.csdn.net/drecik__/article/details/8098633

 

1. Interlocked系列函数:

在用户模式下Windows提供了简单的Interlocked系列函数来提供线程同步的操作,我们无需知道系统是怎么实现这些函数的,只需知道Interlocked系列函数是一个院子操作,在调用的时候,别的线程无法在使用Interlocked系列函数对相同资源进行操作。
例如上一篇博文中出现的问题可以使用下面操作进行线程同步:
long g_x = 0;
DWORD CALLBACK ThreadFunc1( LPVOID lpVoid )
{
	InterlockedExchangeAdd( &g_x, 1 );
	return 0;
}

DWORD CALLBACK ThreadFunc2( LPVOID lpVoid )
{
	InterlockedExchangeAdd( &g_x, 1 );
	return 0;
}
这样就相当于当线程1在执行加法过程中不会被线程2中断,所以能准确的操作资源。
我们使用该系列函数需要注意:
  1. 任何线程都不应该在使用C++语句来修改共享变量,都应该使用Interlocked系列函数来对共享变量进行操作,否则会破坏共享变量的值。
  2. 必须确保传给这些函数的变量地址是经过对齐的,否则这些函数可能会失败,可以使用C运行库_aligned_malloc函数分配一块对齐过的内存
    void * __cdecl _aligned_malloc(
    	size_t _Size,		// 分配的字节数;
    	size_t _Alignment);	// 要对其道的字节边界,必须是2的整数幂次方;
    
    // 使用它来进行释放内存;
    void _aligned_free( void * _Memory);
  3. Interlocked函数执行的很快,通常只占用极少的CPU周期(通常小于50),而且不需要在用户模式和内核模式之间进行切换(该切换通常需要占用1000个周期以上),所以比内核模式的线程同步快很多。
下面介绍下常用的Interlocked函数:
// 变量前加volatile限定符,是为了让编译器不对代码进行优化;
// 每次让变量从所在内存中进行读取;
unsigned long						// 返回为Addend之前的值;
	InterlockedExchangeAdd(
	unsigned long volatile *Addend,	// 待操作变量的指针;
	unsigned long Value				// 加的值,可以为负数;
	);

// InterlockedExchangeAdd的64位版本,参数和返回值功能相同;
LONGLONG
	InterlockedExchangeAdd64(
	__inout LONGLONG volatile *Addend,
	__in    LONGLONG Value
	);

// 相当于InterlockedExchangeAdd给第二个参数传入1;
unsigned long
	InterlockedIncrement(
	unsigned long volatile *Addend
	);

// 相当于InterlockedExchangeAdd给第二个参数传入-1;
unsigned long
	InterlockedDecrement(
	unsigned long volatile *Addend
	);

// 将Value变量的值,赋给Target所指向的变量,返回Target之前的值;
unsigned long
	InterlockedExchange(
	unsigned long volatile *Target,
	unsigned long Value
	);

// 同样有64位版本的InterlockedExchange;
LONGLONG
	InterlockedExchange64 (
	LONGLONG volatile *Target,
	LONGLONG Value
	);

// 将Value指针的值赋给Target所指向的指针变量,返回Target之前的指针值;
// 在32位系统替换的是32位的值,64位则是64位的值;
PVOID
	InterlockedExchangePointer (
	PVOID volatile *Target,
	PVOID Value
	);

// 如果Destination所指向的变量等于Comprand则将Exchange的值赋给Destination所指向的变量;
// 返回原来的变量值,若没复制还是返回原来的值;
unsigned long
	InterlockedCompareExchange(
	unsigned long volatile *Destination,
	unsigned long Exchange,
	unsigned long Comperand
	);

// 64位版本;
LONGLONG
	WINAPI
	InterlockedCompareExchange64 (
	LONGLONG volatile *Destination,
	LONGLONG Exchange,
	LONGLONG Comperand
	);

// 指针版本;
PVOID
	InlineInterlockedCompareExchangePointer (
	PVOID volatile *Destination,
	PVOID ExChange,
	PVOID Comperand
	);

使用InterlockedExchange函数可以实现旋转锁这项技术,来实现对资源的同步访问,如下面代码:

// 全局变量,标志资源是否在使用中;
BOOL g_bResourceInUse = FALSE;
void Func()
{
	// 等待资源可以使用;
	while ( InterlockedExchange( &g_bResourceInUse, TRUE ) == TRUE )
		Sleep(0);

	// 使用资源...;

	// 释放资源;
	InterlockedExchange( &g_bResourceInUse, FALSE );
}


使用这项技术要格外小心,因为旋转锁会消耗CPU时间,CPU必须不断的比较两个值,知道另一个线程改变其中的值,而且这里要假设使用旋转锁的线程都得以相同的优先级运行,我们可以使用SetProcessPriorityBoost或SetThreadPriorityBoost来禁用线程优先级提升。

旋转锁通常保护的资源只是被占用一小段时间,与切换到内核模式然后等待相比,这种情况下以循环方式进行等待的效率会更高。

2. 高速缓存行

如果想为装配有多处理器的及其构建高性能的应用程序,就应该注意高速缓存行。

我们应该根据告诉缓存行的大小将应用程序的数据组织在一起,并将数据域缓存行的边界对其,这样做的目的是为了确保不同的CPU能够各自访问不同的内存地址,而且这些地址不在同一个高速缓存行中。此外我们应该把只读数据域可读写数据分别存放。下面就是一个设计非常糟糕的结构体:

struct CUSTINFO 
{
	DWORD		dwCustomerID;		// 大部分情况下只读;
	int			iBalanceDut;		// 读写;
	wchar_t		szName[100];		// 大部分情况下只读;
	FILETIME	ftLastOrderDate;	// 读写;
};
结构体:如果想确定CPU高速缓存行的大小,可以调用GetLogicalProcessorInformation函数,返回一个SYSTEM_LOGICAL_PROCESSOR_INFORMATION结构数组,可以检查每个Cache字段的LineSize字段,该字段表示CPU的高速缓存行的大小,一旦有了该信息,就可以使用__declspec(align(#))指示符来对字段对齐加以操作,下面是改进了的CUSTINFO结构体:
#define CHCHE_ALIGN 64	//假设为64;
struct __declspec(align(CHCHE_ALIGN)) CUSTINFO 
{
	DWORD		dwCustomerID;		// 大部分情况下只读;
	wchar_t		szName[100];		// 大部分情况下只读;

	__declspec(align(CHCHE_ALIGN))
	int			iBalanceDut;		// 读写;
	FILETIME	ftLastOrderDate;	// 读写;
};

最好是始终只让一个线程访问数据,或者始终只让一个CPU访问数据(使用之前线程与CPU的关联来操作),只要能做到其中的任何一条,就可以完全避免告诉缓存行的问题。

3. 关键段

如果需要以院子方式修改一个值,那么我们应该优先使用Interlocked系列函数,但大部分情况下处理的数据结构要比这负责的多,虽然可以使用旋转锁几只,但是如果被保护的资源运行的时间较长,这样就会浪费CPU时间。

Windows提供了另外一种用户模式的同步机制——关键段。

关键段是一小段代码,它在执行之前需要独占对一些共享资源的访问权,这种方式可以让多行代码以“原子方式”来对资源进行操作。这里的原子方式指的是代码知道除了当前线程以外,没有其他任何线程会同时访问资源,当然,系统仍然可以暂停当前线程去调度其他线程,但是在当前线程离开代码段之前,系统是不会调度任何想要访问关键段资源的其他线程。

关键段的最大好处在于它们使用非常容易,而且内部也使用了Interlocked函数,因此执行速度非常快,但是其最大的缺点在于它们无法用来在多个进程之间对线程进行同步。

下面介绍下关键段的使用函数:

// 初始化关键段结构,传入的参数是一个关键段结构指针;
// 如果有多个资源需要共享,则需要定义多个关键段结构;
VOID InitializeCriticalSection(
	LPCRITICAL_SECTION lpCriticalSection
	);

// 清理关键段结构;
// 如果有资源正在使用该关键段,则将导致不可预料的结果;
VOID DeleteCriticalSection(
	LPCRITICAL_SECTION lpCriticalSection
	);

// 访问资源;
// 如果试图进入一个未初始化的CRITICAL_SECTION,结果将不可预料;
// EnterCriticalSection会执行下面的测试:
//	1. 如果没有线程正在访问资源,那么该函数更新成员变量,以表示调用线程已经;
//		获准了对资源的访问,并立即返回,线程可以继续执行。
//	2. 如果成员变量表示调用线程已经获准资源,则该函数会更新变量,以表示调用;
//		线程获准访问的次数,并立即返回,只有在调用LeaveCriticalSection之前;
//		继续调用了EnterCriticalSection才会发生;
//	3. 如果成员变量表示有一个其他线程已经获准访问资源,那么该函数会使用一个;
//		事件内核对象来吧线程切换到等待状态,从而不会占用CPU时间,直到访问资;
//		源的那线程调用了LeaveCriticalSection,将等待中的线程切换回可调度状态;
VOID EnterCriticalSection(
	LPCRITICAL_SECTION lpCriticalSection
	);

// 试图进入关键段资源,如果成功返回TRUE,并进入资源;
// 失败则返回FALSE,但线程不会被挂起,可以继续执行,其他代码;
BOOL TryEnterCriticalSection(
	LPCRITICAL_SECTION lpCriticalSection
	);

// 离开关键段资源;
// 该函数会检查结构内部的成员变量并将计数器减1,如果计数器大于0,直接返回;
// 如果计数器等于0,该函数会更新成员变量,表示没有线程访问资源,并检查有没;
// 有线程由于调用EnterCriticalSection而处于等待状态,则函数会更新成员变量,;
// 把其中一个线程切换到可调度状态;
VOID LeaveCriticalSection(
	LPCRITICAL_SECTION lpCriticalSection
	);

当线程试图进入一个关键段,但关键段正在被占用的时候,函数会将线程切换到等待状态,意味中将要从用户模式切换到内核模式(大约1000个CPU周期),但是关键段又很有可能很快结束,当切换到内核模式的时候可能就可以访问资源,这样就浪费了大量的CPU时间。

为了提高关键段的性能,Windows把旋转锁合并到了关键段中,当调用EnterCriticalSection时,他首先尝试一段时间内获得资源访问权,时间过后还是失败,则切换到内核模式进入等待状态,可以使用下面函数实现该功能:

// 如果是单CPU电脑,会忽略第二个参数,从而旋转次数为0;
BOOL InitializeCriticalSectionAndSpinCount(
	LPCRITICAL_SECTION lpCriticalSection,
	DWORD dwSpinCount		// 进入等待状态前,旋转锁循环的次数, 0到0x00FFFFFF之间;
	);

// 如果是单CPU电脑,会忽略第二个参数,从而旋转次数为0;
DWORD SetCriticalSectionSpinCount(
	LPCRITICAL_SECTION lpCriticalSection,
	DWORD dwSpinCount
	);

4. Slim读/写锁

SRWLock的目的和关键段的相同,与关键段不同的是SRWLock允许我们哪些想要读取资源(读取者线程)和想要更新资源值的线程(写入者线程),这样可以让所有读取者线程在同一只可访问共享资源,只有当写入者线程需要对资源更新时才需要同步,下面是SRWLock函数:

// 初始化SRWLock,与关键段不同,SRWLock没有删除锁函数,操作系统会自动清理;
VOID InitializeSRWLock (
	__out PSRWLOCK SRWLock
	);

// 写入者访问函数;
// 进入共享资源;
VOID AcquireSRWLockExclusive (
	PSRWLOCK SRWLock
	);

// 离开共享资源;
VOID ReleaseSRWLockExclusive (
	PSRWLOCK SRWLock
	);

// 读取者访问函数;
// 进入共享资源;
VOID AcquireSRWLockShared (
	PSRWLOCK SRWLock
	);

// 离开共享资源;
VOID ReleaseSRWLockShared (
	PSRWLOCK SRWLock
	);


与关键段相比,SRWLock缺乏下面两个特性:

1. 不存在TryEnter之类的函数,如果锁已经被占用,那么直接会阻塞该进程;

2. 不能递归获得SRWLock;

5. 条件变量

目的是为了让线程以院子方式把锁释放并将自己阻塞,知道某个条件成立为止。

条件变量的函数:

// 该函数会释放关键段,在被唤醒的时候又会进入关键段;
BOOL SleepConditionVariableCS (
	PCONDITION_VARIABLE ConditionVariable,	// 等待的条件变量;
	PCRITICAL_SECTION CriticalSection,		// 关键段结构;
	DWORD dwMilliseconds					// 等待时间,可以为INFINITE;
	);

// 该函数会释放SRWlock,在被唤醒的时候又会进入SRWlock;
BOOL SleepConditionVariableSRW (
	PCONDITION_VARIABLE ConditionVariable,
	PSRWLOCK SRWLock,
	DWORD dwMilliseconds,
	ULONG Flags				// 指进入锁的模式,共享还是写入;
	);

// 唤醒一个正在等待条件变量的线程;
VOID WakeConditionVariable (
	__inout PCONDITION_VARIABLE ConditionVariable
	);

// 唤醒所有正在等待条件变量的线程(共享模式下的SRWLock);
VOID WakeAllConditionVariable (
	PCONDITION_VARIABLE ConditionVariable
	);

总结一下,如果希望在应用程序中得到最佳的性能,那么首先应该尝试不要共享数据,然后依次使用volatile 读取,volatile 写入,Interlocked API,SRWLock以及关键段,当且仅当这些都不能满足要求时,再使用内核对象。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值