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

简介: 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 系列函数

       原子访问

           原子 加

           原子 设值

           原子 比较交换地址

       实现旋转锁

   高速缓存行

       结构体设计

   高级线程同步

   关键段

   Slim 读/写锁

       AcquireSRWLockExclusive(&g_lock);

       ReleaseSRWLockExclusive(&g_lock);

   条件变量

       SleepConditionVariableCS/SleepConditionVariableSRW

       WakeConditionVariable/WakeAllConditionVariable

原子访问: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就不必访问内存总线,而访问内存总线需要多得多的时间。

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

   CPU1读取一个字节,这使得该字节以及与它相邻的字节被读到CPU1的高速缓冲行中。

   CPU2读取同一个字节,这使得该字节被读到CPU2的高速缓存行中。

   CPU1对内存中的这个字节进行修改,这使得该字节被写入到CPU1的高速缓存行中,但这一信息还没有写回到内存。

   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结构会作为全局变量来分配,这样进程内的所有线程都可以通过该变量来访问这些结构。实际使用中将此结构作为局部变量、从堆中分配或者是类的私有成员也都是可以的。

但有两个必要条件:

   想要访问资源的线程必须知道用来访问资源的CRITICAL_SECTION结构的地址。

   在任何线程访问被保护的资源之前,必须对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

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

相关文章
|
1月前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
1月前
|
安全 Java UED
深入浅出Java多线程编程
【10月更文挑战第40天】在Java的世界中,多线程是提升应用性能和响应能力的关键。本文将通过浅显易懂的方式介绍Java中的多线程编程,从基础概念到高级特性,再到实际应用案例,带你一步步深入了解如何在Java中高效地使用多线程。文章不仅涵盖了理论知识,还提供了实用的代码示例,帮助你在实际开发中更好地应用多线程技术。
50 5
|
5天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
62 2
|
28天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
26天前
|
缓存 Java 调度
多线程编程核心:上下文切换深度解析
在现代计算机系统中,多线程编程已成为提高程序性能和响应速度的关键技术。然而,多线程编程中一个不可避免的概念就是上下文切换(Context Switching)。本文将深入探讨上下文切换的概念、原因、影响以及优化策略,帮助你在工作和学习中深入理解这一技术干货。
42 10
|
28天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
22天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
22天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
45 3
|
26天前
|
算法 调度 开发者
多线程编程核心:上下文切换深度解析
在多线程编程中,上下文切换是一个至关重要的概念,它直接影响到程序的性能和响应速度。本文将深入探讨上下文切换的含义、原因、影响以及如何优化,帮助你在工作和学习中更好地理解和应用多线程技术。
34 4
|
2天前
|
安全 Java API
【JavaEE】多线程编程引入——认识Thread类
Thread类,Thread中的run方法,在编程中怎么调度多线程