[笔记]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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
第1部分 基础知识   第1章 起步   1.1 Windows环境   1.1.1 Windows简史   1.1.2 Windows的方方面面   1.1.3 动态链接   1.2 Windows编程选项   1.2.1 API及内存管理模式   1.2.2 语言选择   1.2.3 编程环境   1.2.4 API文档   1.3 你的第一个Windows程序   1.3.1 字符模式   1.3.2 Windows对应程序   1.3.3 头文件   1.3.4 程序入口   1.3.5 MessageBox函数   1.3.6 编译、链接及运行   第2章Unicode简介   2.1 字符集简史   2.1.1 美国标准   2.1.2 美国以外的世界   2.1.3 扩展ASCII   2.1.4 双字节字符集   2.1.5 Unicode的解救方案   2.2 宽字符和c语言   2.2.1 char数据类型   2.2.2 更宽的字符   2.2.3 宽字符库函数   2.2.4 维护一个源代码文件   2.3 宽字符和Windows   2.3.1 Windows头文件的类型   2.3.2 Windows函数调用   2.3.3 Windows的字符串函数   2.3.4 在Windows中使用printf   2.3.5 格式化的消息框   2.3.6 国际化之于本书   第3章 窗口与消息   3.1 窗口的创建   3.1.1 系统结构概述   3.1.2 HELLOWIN程序   3.1.3 通盘考虑   3.1.4 窗口类的注册   3.1.5 窗口的创建   3.1.6 窗口的显示   3.1.7 消息循环   3.1.8 窗口过程   3.1.9 消息的处理   3.1.10 声音文件的播放   3.1.11 WM_PAINT消息   3.1.12 WM_DESTROY消息   3.2 Windows编程中的若干难点   3.2.1 究竟是谁调用谁   3.2.2 队列消息和非队列消息   3.2.3 速战速决   第4章 文本输出   4.1 绘制和重绘   4.1.1 WM_PAINT消息   4.1.2 有效矩形和无效矩形   4.2 GDI简介   4.2.1 设备环境   4.2.2 获取设备环境句柄:方法一   4.2.3 绘制信息结构   4.2.4 获取设备环境句柄:方法二   4.2.5 TEXTOUT函数详解   4.2.6 系统字体   4.2.7 字符大小   4.2.8 文本尺寸的度量   4.2.9 文本的格式化   4.2.10 综合使用   4.2.11 SYSMETSl.C窗口过程   4.2.12 空间不够   4.2.13 客户区的尺寸   4.3 滚动条   4.3.1 滚动条的范围和位置   4.3.2 滚动条消息   4.3.3 加入滚动条的SYSMET   4.3.4 程序的绘制代码的结构   4.4 效果更好的滚动   4.4.1 滚动条信息函数   4.4.2 最远可以卷动到哪里?   4.4.3 新的SYSMETS   4.4.4 可我不想用鼠标   第5章 绘图基础   5.1 GDI的结构   5.1.1 GDI原理   5.1.2 GDI函数调用   5.1.3 GDI的基本图形   5.1.4 其他   5.2 设备环境   5.2.1 获取设备环境句柄   5.2.2 获取设备环境的信息   5.2.3 DEVCAPSl程序   5.2.4 设备的尺寸   5.2.5 色彩ABC   5.2.6 设备环境属性   5.2.7 保存设备环境   5.3 点和线的绘制   5.3.1 设定像素   5.3.2 直线   5.3.3 边框绘制函数   5.3.4 贝塞尔样条曲线   5.3.5 使用现有画笔   5.3.6 创建、选择和删除画笔   5.3.7 填充空隙   5.3.8 绘图模式   5.4 绘制填充区域   5.4.1 Polygon函数和多边形填充模式   5.4.2 用画刷填充内部   5.5 GDI映射模式   5.5.1 设备坐标和逻辑坐标   5.5.2 设备坐标系统   5.5.3 视口和窗口   5.5.4 使用MMTEXT   5.5.5 度量映射模式   5.5.6 自定义的映射模式   5.5.7 WHATSIZE程序   5.6 矩形、区域和剪裁   5.6.1 处理矩形   5.6.2 随机矩形   5.6.3 建立和绘制区域   5.6.4 矩形与区域的剪裁   5.6.5 CLOVER程序   第6章 键盘   6.1 键盘基础   6.1.1 忽略键盘   6.1.2 谁获得了焦点?   6.1.3 队列和同步   6.1.4 击键和字符   6.2 击键消息   6.2.1 系统键击和非系统键击   6.2.2 虚拟键代码   6.2.3 1param信息   6.2.4 转义状态   6.2.5 使用击键消息   6.2.6 为SYSMETS加上键盘处理功能   6.3 字符消息   6.3.1 四类字符消息   6.3.2 消息排序   6.3.3 控制字符的处理   6.3.4 死字符消息   6.4 键盘消息和字符集   6.4.1 KEYVIEW1程序   6.4.2 非英语键盘问题   6.4.3 字符集和字体   6.4.4 Unicode解决方案   6.4.5 TrueType字体和大字体   6.5 插入符号(不是光标)   6.5.1 一些关于插入符号的函数   6.5.2 TYPER程序   第7章 鼠标   7.1 鼠标的基础知识   7.1.1 一些基本术语   7.1.2 鼠标的复数形式是什么?   7.2 客户区鼠标消息   7.2.1 简单的鼠标处理示例   7.2.2 处理Shift键   7.2.3 鼠标双击   7.3 非客户区鼠标消息   7.3.1 击中测试消息   7.3.2 消息引发消息   7.4 程序中的击中测试   7.4.1 一个假想的例子   7.4.2 一个简单的程序   7.4.3 使用键盘模仿鼠标操作   7.4.4 在CHECKER中增加键盘接口   7.4.5 在击中测试中使用子窗口   7.4.6 CHECKER程序中的子窗口   7.4.7 子窗口和键盘   7.5 捕获鼠标   7.5.1 设计一个矩形   7.5.2 捕获的解决方案   7.5.3 BLOKOUT2程序   7.6 鼠标的滚轮   第8章 计时器   8.1 计时器的基本知识   8.1.1 系统和计时器   8.1.2 计时器消息不是异步的   8.2 使用计时器的三种方法   8.2.1 方法一   8.2.2 方法二   8.2.3 方法三   8.3 使用计时器作为时钟   8.3.1 数字时钟   8.3.2 获取当前时间   8.3.3 显示数字和冒号   8.3.4 考虑国际化   8.3.5 模拟时钟   8.4 在状态报告上使用计时器   第9章 子窗口控件   9.1 按钮类   9.1.1 创建子窗口   9.1.2 子窗口传递信息给父窗口   9.1.3 父窗口传递信息给子窗口   9.1.4 按钮   9.1.5 复选框   9.1.6 单选按钮   9.1.7 组合框   9.1.8 改变按钮文本   9.1.9 可见的按钮和启用的按钮   9.1.10 按钮和输入焦点   9.2 控件和颜色   9.2.1 系统颜色   9.2.2 按钮的颜色   9.2.3 WMCTLCOLORBTN消息   9.2.4 自绘按钮   9.3 静态类   9.4 滚动条类   9.4.1 COLORS1程序   9.4.2 自动键盘接口   9.4.3 窗口子类   9.4.4 背景着色   9.4.5 给滚动条和静态文本着色   9.5 编辑类   9.5.1 编辑类的样式   9.5.2 编辑控件的通知消息   9.5.3 使用编辑控件   9.5.4 传递给编辑控件的消息   9.6 列表框类   9.6.1 列表框的样式   9.6.2 向列表框中添加字符串   9.6.3 项目的选择和提取   9.6.4 接收来自列表框的消息   9.6.5 简单的列表框程序   9.6.6 列出文件   9.6.7 Windows的HEAD程序   第10章 菜单和其他资源   10.1 图标、鼠标指针、字符串和自定义资源   10.1.1 向程序添加图标   10.1.2 获得图标的句柄   10.1.3 在应用程序中使用图标   10.1.4 使用自定义鼠标指针   10.1.5 字符串资源   10.1.6 自定义资源   10.2 菜单   10.2.1 和菜单有关的概念   10.2.2 菜单结构   10.2.3 定义菜单   10.2.4 在程序中引用菜单   10.2.5 菜单和消息   10.2.6 范例程序   10.2.7 菜单设计中的规范   10.2.8 定义菜单的繁琐方式   10.2.9 浮动弹出菜单   10.2.1 0使用系统菜单   10.2.1 1改变菜单   10.2.1 2其他菜单命令   10.2.1 3菜单的另类用法   10.3 键盘加速键   10.3.1 为什么你应该使用键盘加速键   10.3.2 指定加速键的一些规则   10.3.3 加速键表   10.3.4 加载加速键表   10.3.5 翻译按键   10.3.6 接收加速键消息   10.3.7 带有菜单和加速键的POPPAD程序   10.3.8 启用菜单项   10.3.9 处理菜单项   第11章 对话框   11.1 模态对话框   11.1.1 创建一个About对话框   11.1.2 对话框及其模板   11.1.3 对话框过程   11.1.4 激活对话框   11.1.5 主题变换   11.1.6 更复杂的对话框   11.1.7 对话框控件的应用   11.1.8 OK和Cancel按钮   11.1.9 避免全局变量   11.1.1 0Tab停靠和选项组   11.1.1 1在对话框上绘图   11.1.1 2关于对话框的其他函数.   11.1.1 3定义程序自己的控件   11.2 非模态对话框   11.2.1 模态与非模态对话框的区别   11.2.2 新的COLORS程序   11.2.3 HEXCALC:窗口还是对话框?   11.3 公用对话框   11.3.1 完善POPPAD   11.3.2 Unicode文件的读/写操作   11.3.3 改变字体   11.3.4 查找和替换   11.3.5 只调用一个函数的Windows程序   ……   第Ⅱ部分 关于图的那些事儿   第Ⅲ部分 高级主题

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

二进制怪兽

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

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

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

打赏作者

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

抵扣说明:

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

余额充值