用户模式下的线程同步的分析(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; ...
随机推荐
- CVE-2017-12149-JBoss 5.x/6.x 反序列化
漏洞分析 https://www.freebuf.com/vuls/165060.html 漏洞原理 该漏洞位于JBoss的HttpInvoker组件中的 ReadOnlyAccessFilter 过 ...
- [LeetCode 279.] Perfect Squres
LeetCode 279. Perfect Squres DP 是笨办法中的高效办法,又是一道可以被好办法打败的 DP 题. 题目描述 Given a positive integer n, find ...
- WPF 基础 - Trigger
1. Trigger 1.1 由属性值触发的 Trigger 最基本的触发器,Property 是关注的属性名称,value 是触发条件,一旦触发条件满足,就会应用 Trigger 的 Setters ...
- slickgrid ( nsunleo-slickgrid ) 1 开篇有益
slickgrid (nsunleo-slickgrid) 1 开篇有益 作为专职的程序猿,自认为是老菜鸟或老民工,以前一直在某浪上写博客,上知天文,下达地理.做了N年的.net,又转Java,从 ...
- 时间&空间(complexity)
时间&空间复杂度 时间复杂度: 通俗来说就是随着数据量的增加,程序运行的时间花费量是怎么变化的,时间复杂度常用大o表示.举个例子,猜数字,猜10个,100个.1000个,猜数的数据量是在增加的 ...
- teprunner测试平台用例前置模块开发
本文开发内容 现在正式进入测试相关功能开发.teprunner测试平台底层是pytest,中间层是tep,还没了解的朋友可以先看看tep的文章,整个平台的设计思路和后面用例的执行都会基于这个工具.te ...
- Java例题_39 判断奇偶后分数累加
1 /*39 [程序 39 分数累加] 2 题目:编写一个函数,输入 n 为偶数时,调用函数求 1/2+1/4+...+1/n,当输入 n 为奇数时,调用函数 3 1/1+1/3+...+1/n 4 ...
- Java例题_17 猴子偷桃问题
1 /*17 [程序 17 猴子吃桃问题] 2 题目:猴子吃桃问题 3 猴子第一天摘下若干个桃子,当即吃了一半,还不瘾,又多吃了一个 4 第二天早上又将剩下的桃子吃掉一半,又多吃了一个. 5 以后每天 ...
- C++并发与多线程学习笔记--多线程数据共享问题
创建和等待多个线程 数据和共享问题分析 只读的数据 有读有写 其他案例 共享数据的保护案例代码 创建和等待多个线程 服务端后台开发就需要多个线程执行不同的任务.不同的线程执行不同任务,并返回执行结果. ...
- 第一个真正的 GUI 程序——Tkinter教程系列02
第一个真正的 GUI 程序--Tkinter教程系列02 前言 欢迎光临我的个人博客 chens.life Tk 系列教程: Tkinter教程系列01--引言和安装Tk 我们将编写一个英尺和米的转换 ...