用户模式下的线程同步的分析(Windows核心编程)
线程同步
- 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源,运气好一点的情况下得到的值应该为 2,运气不好的情况下就会变为 1
#include <Windows.h>
#include <iostream>
#include <process.h>
using namespace std;
DWORD WINAPI ThreadCommunication();
unsigned __stdcall ThreadFun1(void* pvParam);
unsigned __stdcall ThreadFun2(void* pvParam);
// 共享资源
int Statistics = 0;
int main(int argc, char *argv[])
{
ThreadCommunication();
return 0;
}
DWORD WINAPI ThreadCommunication()
{
// 创建线程
HANDLE myThread1 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun1, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread1);
HANDLE myThread2 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun2, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread2);
// 等待线程退出
HANDLE Threads[2] = { 0 }; Threads[0] = myThread1; Threads[1] = myThread2;
WaitForMultipleObjects(2, Threads, TRUE, INFINITE);
// 关闭线程句柄
CloseHandle(myThread1); CloseHandle(myThread2);
// 最后打印计数
cout << "Statistics: " << Statistics << endl;
return TRUE;
}
// 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 做一些事情,完成之后计数加一
Statistics++;
return TRUE;
}
// 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 做一些事情,完成之后计数加一
Statistics++;
return TRUE;
}
- 这个就是运气不好的情况,原因就是多线程运行时对同一资源的访问是竞争性的,线程 1 访问资源的时候线程 2 可能被堵塞。那么怎么让线程进行对同一资源有顺序的访问呢,微软并没有提供这样的方法可以让第一个线程先运行或者当第二个线程先运行(Windows系统并不是实时操作系统,对线程按照算法公平的原则进行调度,是随机的),但是微软提供了一些方法可以让线程同步运行,保证资源的按顺序访问

- 线程同步逻辑图

0x01 原子访问
- 原子访问是解决线程冲突的第一种方法,原子访问顾名思义就是按原子的方式访问资源并且保证访问资源不冲突。那么上面的代码可以改为这样:
// 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 做一些事情,完成之后计数加一
InterlockedExchangeAdd(&Statistics, 1);
return TRUE;
}
// 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 做一些事情,完成之后计数加一
InterlockedExchangeAdd(&Statistics, 1);
return TRUE;
}
- InterlockedExchangeAdd 的第一个参数可以传入一个 long 型变量,第二个参数传入想要加的值(只可以是整数,需要减的话传负数即可),该函数是以原子方式操作,所以解决了访问冲突的问题;如果嫌麻烦可以使用 InterlockedIncrement 函数直接将变量加一
- 但这些 Interlock 函数存在一个缺点,只能加减固定的整数,所以不够灵活(总不能在使用函数之前花时间做运算吧),所以出现了升级版函数 InterlockedExchange,这个函数可以直接把变量以原子方式改为我们想要的值,当然了这个是 32 位的,如果需要 64 位的可以使用 InterlockedExchange64 这个函数
// 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 做一些事情, 之后将 Statistics 的值变为 1
InterlockedExchange(&Statistics, 1);
return TRUE;
}
// 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 做一些事情, 之后将 Statistics 的值变为 2
InterlockedExchange(&Statistics, 2);
return TRUE;
}
- 除了以原子方式加减或改变一个变量,微软还给出了比较方便的比较函数
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 做一些事情
InterlockedExchange(&Statistics, 1);
return TRUE;
}
// 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 做一些事情
InterlockedCompareExchange(&Statistics, Statistics + 1, 1);
return TRUE;
}
- 上面代码的意义是如果线程 1 先运行并且将 Statistics 的值设置为 1 的话,线程 2 就将现有的 Statistics 的值加 1
0x02 关键段和旋转锁
- 纵观单用原子的方式解决线程同步确实有很大的局限性,第一:对资源变量只能以整数的方式进行操作;第二:只能更改变量,不能做到更改其他资源,比如 IO 读写等。所以出现了解决线程同步的第二种方法关键段和旋转锁
- 关键段顾名思义就是在这个代码段中,代码访问的所有资源都是按原子方式进行的,防止线程之间的访问冲突,实现关键段的方式也很简单:
#include <Windows.h>
#include <iostream>
#include <process.h>
using namespace std;
DWORD WINAPI ThreadCommunication();
unsigned __stdcall ThreadFun1(void* pvParam);
unsigned __stdcall ThreadFun2(void* pvParam);
// 共享资源
int Statistics = 0;
// 创建供关键段使用的结构体
CRITICAL_SECTION Critical;
int main(int argc, char *argv[])
{
ThreadCommunication();
return 0;
}
DWORD WINAPI ThreadCommunication()
{
// 初始化结构体
InitializeCriticalSection(&Critical);
// 创建线程
HANDLE myThread1 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun1, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread1);
HANDLE myThread2 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun2, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread2);
// 等待线程退出
HANDLE Threads[2] = { 0 }; Threads[0] = myThread1; Threads[1] = myThread2;
WaitForMultipleObjects(2, Threads, TRUE, INFINITE);
// 关闭线程句柄
CloseHandle(myThread1); CloseHandle(myThread2);
// 清除关键段结构体
DeleteCriticalSection(&Critical);
// 最后打印计数
cout << "Statistics: " << Statistics << endl;
return TRUE;
}
// 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 关键段开始
EnterCriticalSection(&Critical);
// 做一些事情
for (size_t i = 0; i < 1000; i++)
{
Statistics++;
}
// 关键段结束
LeaveCriticalSection(&Critical);
return TRUE;
}
// 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 关键段开始
EnterCriticalSection(&Critical);
// 做一些事情
for (size_t i = 0; i < 1000; i++)
{
Statistics++;
}
// 关键段结束
LeaveCriticalSection(&Critical);
return TRUE;
}
- 需要注意的是在将结构体传入 EnterCriticalSection 或者 LeaveCriticalSection 函数之前必须使用 InitializeCriticalSection 函数对 CRITICAL_SECTION 结构体进行初始化。共享变量访问结束之后记得使用 DeleteCriticalSection 函数清除 CRITICAL_SECTION 结构体
注:任何线程都可以使用 LeaveCriticalSection 和 EnterCriticalSection 函数开辟关键段来防止对共享资源的访问冲突。但这里有一个问题就是如果一个线程抢到了对某一个共享资源的 CPU 访问时间,那么其它线程在想访问这一个共享资源是就会被阻塞,也就是会一直等待,所以为了节省宝贵的 CPU 时间,微软给出了 TryEnterCriticalSection 函数来解决以上问题,如果共享资源被别的线程访问,那么 TryEnterCriticalSection 函数就会直接返回 FALSE 而不是等待,假如共享资源并没有被其他线程访问,那么 TryEnterCriticalSection 函数就会返回 TRUE 并且更新 CRITICAL_SECTION 的成员变量,当然之后需要调用 LeaveCriticalSection 函数来释放CRIRICAL_SECTION 结构体变量
- 旋转锁是基于原子访问的技术,简单来说就是利用更为复杂的原子访问实现旋转锁:
long res = FALSE;
// 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 使用旋转锁
while (InterlockedExchange(&res, TRUE) == TRUE)
Sleep(0);
// 做一些事情
InterlockedExchange(&res, FALSE);
return TRUE;
}
// 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 使用旋转锁
while (InterlockedExchange(&res, TRUE) == TRUE)
Sleep(0);
// 做一些事情
InterlockedExchange(&res, FALSE);
return TRUE;
}
- 如果线程 1 先运行,那么 InterlockedExchange 会将 res 的值变为 TRUE,并且返回值为 TRUE,之后线程 1 就可以做它的工作了和访问共享的资源了;如果这时线程 2 运行,使用 InterlockedExchange 修改 res 为 TRUE 就会返回 FALSE(因为 res 已经为 TRUE)
注:关键段的使用需要注意死锁这个问题
0x03 读写锁和条件变量
- 读写锁类似于关键段,也是在保护对共享资源的访问,具体实现如下
#include <Windows.h>
#include <iostream>
#include <process.h>
using namespace std;
DWORD WINAPI ThreadCommunication();
unsigned __stdcall ThreadFun1(void* pvParam);
unsigned __stdcall ThreadFun2(void* pvParam);
// 共享资源
int Statistics = 0;
// 读写锁结构体
SRWLOCK Srwlock;
int main(int argc, char *argv[])
{
ThreadCommunication();
return 0;
}
DWORD WINAPI ThreadCommunication()
{
// 使用 InitalizeSRWLock 函数初始化结构体
InitializeSRWLock(&Srwlock);
// 创建线程
HANDLE myThread1 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun1, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread1);
HANDLE myThread2 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun2, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread2);
// 等待线程退出
HANDLE Threads[2] = { 0 }; Threads[0] = myThread1; Threads[1] = myThread2;
WaitForMultipleObjects(2, Threads, TRUE, INFINITE);
// 关闭线程句柄
CloseHandle(myThread1); CloseHandle(myThread2);
// 最后打印计数
cout << "Statistics: " << Statistics << endl;
return TRUE;
}
// 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 写入这线程
AcquireSRWLockExclusive(&Srwlock);
// 做一些事情
Statistics++;
ReleaseSRWLockExclusive(&Srwlock);
return TRUE;
}
// 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 读取者线程
AcquireSRWLockShared(&Srwlock);
// 做一些事情
Statistics++;
ReleaseSRWLockShared(&Srwlock);
return TRUE;
}
- 与关键段不同的是,读写锁并没有提供函数来取消等待,而关键段的 TryEnterCriticalSection 函数则可以;但是读写锁也有便于控制线程访问资源的好处,比如控制一些线程只读共享资源,另一些线程更新共享资源,增加了程序的可伸缩性;权衡利弊还是优先使用关键段
- 对于线程同步的最后一种方式就是条件变量了,想象这样一种场景:某一个线程的功能是载入用于输入的一个文件,如果用户已经输入了这个文件的路径,那么载入它;如果用户还没有输入,那么线程进入等待状态,直到用户进行了输入。那么就利用条件变量实现它吧
#include <Windows.h>
#include <iostream>
#include <process.h>
using namespace std;
DWORD WINAPI ThreadCommunication();
unsigned __stdcall ThreadFun1(void* pvParam);
unsigned __stdcall ThreadFun2(void* pvParam);
// 读写锁结构体
SRWLOCK Srwlock;
int main(int argc, char *argv[])
{
ThreadCommunication();
return 0;
}
DWORD WINAPI ThreadCommunication()
{
// 使用 InitalizeSRWLock 函数初始化结构体
InitializeSRWLock(&Srwlock);
// 创建线程
HANDLE myThread1 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun1, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread1);
HANDLE myThread2 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun2, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread2);
// 等待线程退出
HANDLE Threads[2] = { 0 }; Threads[0] = myThread1; Threads[1] = myThread2;
WaitForMultipleObjects(2, Threads, TRUE, INFINITE);
// 关闭线程句柄
CloseHandle(myThread1); CloseHandle(myThread2);
return TRUE;
}
// 模拟判断用户是否输入
BOOL IsFit = FALSE;
// 条件变量结构体
CONDITION_VARIABLE Condition;
// 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
AcquireSRWLockExclusive(&Srwlock);
// 模拟用户输入
Sleep(300);
// 输入完成之后将 IsFit 变为 TRUE,并且唤醒等待线程
IsFit = TRUE;
WakeConditionVariable(&Condition);
ReleaseSRWLockExclusive(&Srwlock);
return TRUE;
}
// 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
AcquireSRWLockExclusive(&Srwlock);
// 如果用户还没有输入则进入等待状态,等待用户输入
if (IsFit == FALSE)
{
cout << "[-] 等待用户输入: " << endl;
SleepConditionVariableSRW(&Condition, &Srwlock, INFINITE, 0);
}
cout << "[*] 用户输入完毕 " << endl;
// 根据用户输入载入文件...
ReleaseSRWLockExclusive(&Srwlock);
return TRUE;
}
- 如果线程二执行前,用户已经输入并且变量 IsFit 的值已经变为 TRUE,那么没有问题,按照流程载入文件即可;如果线程二执行前用户还没有输入,那么该线程就会进入等待状态直到用于输入完成之后才开始往下执行载入文件的操作
用户下的线程用户研究总结到此完毕,如有错误欢迎指正
参考资料:Windows 核心编程
用户模式下的线程同步的分析(Windows核心编程)的更多相关文章
- windows核心编程---第七章 用户模式下的线程同步
用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...
- 《windows核心编程系列》七谈谈用户模式下的线程同步
用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...
- 【windows核心编程】 第八章 用户模式下的线程同步
Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ① 需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ② 一个线程需要通知其他线程 ...
- Windows核心编程:第8章 用户模式下的线程同步
Github https://github.com/gongluck/Windows-Core-Program.git //第8章 用户模式下的线程同步.cpp: 定义应用程序的入口点. // #in ...
- 内核模式下的线程同步的分析(Windows核心编程)
内核模式下的线程同步 内核模式下的线程同步是用户模式下的线程同步的扩展,因为用户模式下的线程同步有一定的局限性.但用户模式下线程同步的好处是速度快,不需要切换到内核模式(需要额外的 CPU 时间).通 ...
- 第8章 用户模式下的线程同步(4)_条件变量(Condition Variable)
8.6 条件变量(Condition Variables)——可利用临界区或SRWLock锁来实现 8.6.1 条件变量的使用 (1)条件变量机制就是为了简化 “生产者-消费者”问题而设计的一种线程同 ...
- 第8章 用户模式下的线程同步(1)_Interlocked系列函数
8.1 原子访问:Interlocked系列函数(Interlock英文为互锁的意思) (1)原子访问的原理 ①原子访问:指的是一线程在访问某个资源的同时,能够保证没有其他线程会在同一时刻访问该资源. ...
- 第8章 用户模式下的线程同步(3)_Slim读写锁(SRWLock)
8.5 Slim读/写锁(SRWLock)——轻量级的读写锁 (1)SRWLock锁的目的 ①允许读者线程同一时刻访问共享资源(因为不存在破坏数据的风险) ②写者线程应独占资源的访问权,任何其他线程( ...
- 第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)
8.4 关键段(临界区)——内部也是使用Interlocked函数来实现的! 8.4.1 关键段的细节 (1)CRITICAL_SECTION的使用方法 ①CRITICAL_SECTION cs; ...
随机推荐
- Docker安装开发环境
目录 Docker Docker 安装 Mysql Docker 安装Redis Docker 安装Zookeeper Docker Docker 安装 Mysql Docker 查看可用Mysql镜 ...
- Pyqt5学习笔记(一)
Python已有的GUI框架: Tkinter(python内嵌的GUI环境,使用TCL实现,易学易用,方便简单创GUI自带无需安装,适用于Unix.Windows和Mac系统组,在Tk8.0的后续版 ...
- [SPOJ2021] Moving Pebbles
[SPOJ2021] Moving Pebbles 题目大意:给你\(N\)堆\(Stone\),两个人玩游戏. 每次任选一堆,首先拿掉至少一个石头,然后移动任意个石子到任意堆中. 谁不能移动了,谁就 ...
- API管理工具
开源的api文档管理系统 api文档 php 在项目中,需要协同开发,所以会写许多API文档给其他同事,以前都是写一个简单的TXT文本或Word文档,口口相传,这种方式比较老土了,所以,需要有个api ...
- 怎么理解onStart可见但不可交互
前言 今天朋友遇到一个面试题,分享给大家: onStart生命周期表示Activity可见,那为什么不能交互呢? 这个问题看似简单,但涉及到的面还是比较多的,比如Activity生命周期的理解,进程的 ...
- DenseNet的个人总结
DenseNet这篇论文是在ResNet之后一年发表的,由于ResNet在当时引起了很大的轰动,所以DenseNet也将ResNet作为了主要的对比方法,读起来还是比较容易的,全篇只有两个数学公式,也 ...
- 数据库期末作业之银行ATM存取款机系统
--一.建库.建表.建约束 --1.使用SQL创建表 --客户信息表userinfo --字段名称 说明 备注 --customerID 顾客编号 自动编号(标识列),从1开始,主键 --用序列seq ...
- MVC中"删除"按钮无法实现
出现原因:MVC视图中定义了空的模板页 解决办法:删除模板页 或 改成定义页面标题都可以
- PTA 线性表元素的区间删除
6-8 线性表元素的区间删除 (20 分) 给定一个顺序存储的线性表,请设计一个函数删除所有值大于min而且小于max的元素.删除后表中剩余元素保持顺序存储,并且相对位置不能改变. 函数接口定义: ...
- springMVC:校验框架:多规则校验,嵌套校验,分组校验;ssm整合技术
知识点梳理 课堂讲义 学习目标 能够阐述表单验证的分类和区别 能够运用表单验证的常用注解 能够编写表单验证的示例 能够编写SSM整合的应用案例 能够总结SSM整合的步骤 1 校验框架 1.1 入门-视 ...