[笔记]Windows核心编程《七》用户模式下的线程同步

系列文章目录

[笔记]Windows核心编程《一》错误处理、字符编码
[笔记]Windows核心编程《二》内核对象
[笔记]Windows核心编程《三》进程
[笔记]Windows核心编程《四》作业
[笔记]快乐的LInux命令行《五》什么是shell
[笔记]Windows核心编程《五》线程基础
[笔记]Windows核心编程《六》线程调度、优先级和关联性
[笔记]Windows核心编程《七》用户模式下的线程同步
[笔记]Windows核心编程《八》用内核对象进行线程同步
[笔记]Windows核心编程《九》同步设备I/O和异步设备I/O
[笔记]Windows核心编程《十一》Windows线程池
[笔记]Windows核心编程《十二》纤程
[笔记]Windows核心编程《十三》windows内存体系结构
[笔记]Windows核心编程《十四》探索虚拟内存
[笔记]Windows核心编程《十五》在应用程序中使用虚拟内存
[笔记]Windows核心编程《十六》线程栈
[笔记]Windows核心编程《十七》内存映射文件
[笔记]Windows核心编程《十八》堆栈
[笔记]Windows核心编程《十九》DLL基础
[笔记]Windows核心编程《二十》DLL的高级操作技术
[笔记]Windows核心编程《二十一》线程本地存储器TLS
[笔记]Windows核心编程《二十二》注入DLL和拦截API
[笔记]Windows核心编程《二十三》结构化异常处理

相关:

原子访问:Interlocked 系列函数

原子访问

原子 加

原子访问就是,一个线程在访问某个资源的同时保证没有其他线程会在同一时刻访问同一资源。"原子性"就是在原子访问中途不能被打断。
windows提供Interlocked系列函数实现这一功能。

LONG InterlockedExchangeAdd(
   PLONG volatile *plAddend,   //要计算的长整形变量的地址
   LONG          lIncrement   //指定的长整形增量
);

这个函数对传入的变量(*plAddend)增加lIncrement的值,volatile要求对系统不进行优化,每次都从内存中读取,而不是寄存器。
volatile:
要求对系统不进行优化,每次都从内存中读取,而不是寄存器。

为什么需要线程同步?
为了避免在一线程对以数据操作过程中(一进行一部分操作但是尚未完成)CPU时间片耗尽当前线程挂起时,另一进程对修改不完全的数据进行操作。

原子 设值

LONG InterlockedExchange(
   PLONG volatile *plTarget,   //要赋值的变量地址
   LONG          lValue   //要赋的值
);

设置一个32位的变量设置为指定的值。
返回TRUE表示修改成功;FALSE表示失败。

原子 比较交换地址

PVOID InterlockedCompareExchangePointer(
  PVOID volatile *Destination,
  PVOID          Exchange,
  PVOID          Comperand
);

如果Destination与Comparand相等,那么就把Destination置为Exchange,其他情况,Destination不变。反回的是原来Destation指向的值,这点一定要注意。

实现旋转锁

BOOL g_fResourceInUse = FALSE;
void Func1(){
	//Wait to Access the resource
	while(InterlockedExchange(&g_fResourceInUse,TRUE) == TRUE )
	{
		Sleep(0);
	}
	//Access the Resource
	...
	InterlockedExchange(&g_fResourceInUse,FALSE);
}

使用旋转锁应该注意的地方:
(1)使用这种同步方式,我们需要保证需要同步的进程是运行在同一优先级的,如果两个线程优先级为6,另一个为4,那么优先级为4的线程不会得到等待资源的机会。
(2)应该避免资源标示和资源本身在同一告诉缓存行 (3)应避免在单处理器的系统上使用旋转锁

旋转锁假定被保护的资源始终只会占用一小段时间。与切换内核模式然后等待相比,在这种情况下以循环的方式进行等待的效率更高。 许多开发人员会循环指定的次数(比如4000),如果届时仍然无法访问资源,那么线程会切换到内核模式,并一直等待到资源可供使用为止(此时它不消耗CPU时间),这就是关键段(Critical Section)的实现方式。

在多处理器的机器上旋转锁比较有用,这是因为当一个线程在CPU上运行的时候,另一个线程可以在另一个CPU上循环等待,

高速缓存行

当一个C P U从内存读取一个字节时,它不只是取出一个字节,它要取出足够的字节来填入高速缓存行。高速缓存行的作用是为了提高C PU 运行的性能。通常情况下,应用程序只能对一组相邻的字节进行处理。如果这些字节在高速缓存中,那么CPU就不必访问内存总线,而访问内存总线需要多得多的时间。

但是在多处理器环境下,高速缓存线使得对内存的更新变得更加困难。

  1. CPU1读取一个字节,这使得该字节以及与它相邻的字节被读到CPU1的高速缓冲行中。
  2. CPU2读取同一个字节,这使得该字节被读到CPU2的高速缓存行中。
  3. CPU1对内存中的这个字节进行修改,这使得该字节被写入到CPU1的高速缓存行中,但这一信息还没有写回到内存。
  4. CPU2再次读取同一个字节,由于该字节已经在CPU2的高速缓存行中,因此CPU2不需要再访问内存。但CPU2将无法看到该字节在内存中新的值。

明确地说,当一个CPU修改了高速缓存行中的一个字节时,机器中的其他CPU会收到通知,并使自己的高速缓存行作废。当CPU1修改该字节的值时,CPU2的高速缓存就作废了,在第四步中,CPU1必须将它的高速缓存写回内存中,CPU2 必须重新访问内存来填满它的高速缓存行。
高速缓存行虽然能够提高性能,但在多处理器的机器上它们同样能够损伤性能。

结构体设计

读写混放的数据结构(只读与读写的数据放入同一个高速缓存,不好的做法):

struct  CUSTINFO
 {
   DWORD    dwCustomerID;    //Mostly read-only
   int      nBalanceDue;     //Read-write
   char     szName[100];     //Mostly read-only
   FILETIME ftLastOrderDate;  //Read-write
};

改版后:

 //  Determine the cache line size for the host CPU.
 // 为各种CPU定义告诉缓存行大小
 #ifdef _X86_
 #define  CACHE_ALIGN  32
 #endif
#ifdef _ALPHA_
 #define  CACHE_ALIGN  64
 #endif
#ifdef _IA64_
 #define  CACHE_ALIGN  ??
 #endif

 #define  CACHE_PAD(Name, BytesSoFar) \
   BYTE Name[CACHE_ALIGN  -  ((BytesSoFar)  %  CACHE_ALIGN)]

 struct  CUSTINFO
 {
   DWORD    dwCustomerID;     // Mostly read-only
   char     szName[100];      // Mostly read-only

   //Force the following members to be in a different cache line.
   //这句很关键用一个算出来的Byte来填充空闲的告诉缓存行
   //如果指定了告诉缓存行的大小可以简写成这样
   //假设sizeof(DWORD) + 100 = 108;告诉缓存行大小为32
   //BYTE[12];
   //作用呢就是强制下面的数据内容与上面数据内容不在同一高速缓存行中。
   CACHE_PAD(bPad1, sizeof(DWORD) + 100);

   int      nBalanceDue;      // Read-write
   FILETIME ftLastOrderDate;  // Read-write

   //Force the following structure to be in a different cache line.
   CACHE_PAD(bPad2, sizeof(int) + sizeof(FILETIME));
} ;

高级线程同步

应该避免的一种同步方法:

volatile  BOOL g_fFinishedCalculation  =  FALSE;

 int  WINAPI WinMain()
 {
   CreateThread(, RecalcFunc, );
   
   //Wait for the recalculation to complete.
   while(!g_fFinishedCalculation)
        ;
   
 }

DWORD WINAPI RecalcFunc(PVOID pvParam)
 {
   //Perform the recalculation.
   
   g_fFinishedCalculation = TRUE;
   return(0);
}

问题:
(1)主线程没有进入睡眠时间,CPU会一直调度时间轮片给他,从其他线程手中夺走了宝贵的CPU时间。
(2)主线程一直在占用CPU,其他比主线程优先级低的线程根本无法执行。主线程优先级必须和新建线程优先级相同,否则g_fFinishedCalculation永远不会被置True,主线程将无限循环

volatile关键字的作用,告诉编译器不要对变量进行任何的优化,始终在内存中获取fFinishedCalculation的值。

关键段

关键段是一小段代码,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以原子方式访问,在当前线程离开关键段之前,系统不会调度任何其他线程访问该关键段。

比如在一个链表管理的例子中,如果对链表的访问没有同步,那么一个线程可能会在另一个线程在链表中查询时向链表添加元素。如果两个线程同时向链表中添加元素情况会更糟。而使用关键段可以有效地防止以上各种情况。

要使用关键段首先需要定义CRITICAL_SECTION结构。然后把任何需要共享的代码放在EnterCriticalSection和LeaveCriticalSection之间。

DWORD WINAPI ThreadProc1(PVOID)
{
EnterCriticalSection(&g_a);
for(int i=0;i<100;i++)
  g_a++;
LeaveCriticalSection(&cs);
 return 0;
}

关键段由于在内部使用了Interlock系列函数因此执行速度非常快。它的缺点是不能在多进程之间对线程进行同步。而信号量和事件则可以。

一般情况下CRITICAL_SECTION结构会作为全局变量来分配,这样进程内的所有线程都可以通过该变量来访问这些结构。实际使用中将此结构作为局部变量、从堆中分配或者是类的私有成员也都是可以的。
但有两个必要条件:

  1. 想要访问资源的线程必须知道用来访问资源的CRITICAL_SECTION结构的地址。
  2. 在任何线程访问被保护的资源之前,必须对CRITICAL_SECTION结构进行初始化。初始化调用:
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);

此函数会设置CRITICAL_SECTION结构的一些成员。如果这些成员没有经过初始化,结果将是不可预料的。
当线程不需要访问共享资源时,应该调用以下函数来清理CRITICAL_SECTION结构:

VOID DeleteCriticalSection(PCRITICAL_SECTION pcs);

为了防止当资源被其他线程占用时,主调线程被挂起,可以调用TryEnterCriticalSection函数,该函数将会检测此时主调线程是否可以访问共享资源。如果资源被占用该函数返回false。否则返回true。
如果检测到资源此时已被占用,主调线程这是可以做其他事情,而不是被挂起。由于TryEnterCriticalSection函数会更新CRITICAL_SECTION结构的某些成员,因此需要对应一个LeaveCriticalSection函数。

Slim 读/写锁

SRWLock 的目的和关键段相同:对一个资源进行保护,不让其他线程访问它。
但是SRWLock 它跟关键段有所不同,读写锁允许我们区分那些想要读取资源的线程和更改资源的线程。
让所有的读取资源的线程同时工作是可行的,因为读取不会破坏数据。只有当写入时才需要对写入线程进行同步。
写入线程必须独占资源,它工作时无论是读取还是写入线程都必须等待。

AcquireSRWLockExclusive(&g_lock);

请求占用读写锁

ReleaseSRWLockExclusive(&g_lock);

请求释放读写锁

条件变量

我们希望线程以原子的方式把锁释放并将自己阻塞,直到某一个条件成立为止。要实现这样的线程同步是比较复杂的。windows通过SleepConditionVariableCS(critical section)或者SleepConditionVariableSRW函数,提供了一种条件变量帮助我们完成这项工作。
当线程检测到相应的条件满足的时候(比如,由数据供读取者使用),他会调用WakeConditionVariable或WakeAllConditionVariable,
这样在Sleep*函数中的线程就会被唤醒。

SleepConditionVariableCS/SleepConditionVariableSRW

使用条件变量睡眠关键段或者读写锁

WakeConditionVariable/WakeAllConditionVariable

使用条件变量唤醒关键段或者读写锁

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二进制怪兽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值