线程同步

  • 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源,运气好一点的情况下得到的值应该为 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核心编程)的更多相关文章

  1. windows核心编程---第七章 用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  2. 《windows核心编程系列》七谈谈用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  3. 【windows核心编程】 第八章 用户模式下的线程同步

    Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ①    需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ②    一个线程需要通知其他线程 ...

  4. Windows核心编程:第8章 用户模式下的线程同步

    Github https://github.com/gongluck/Windows-Core-Program.git //第8章 用户模式下的线程同步.cpp: 定义应用程序的入口点. // #in ...

  5. 内核模式下的线程同步的分析(Windows核心编程)

    内核模式下的线程同步 内核模式下的线程同步是用户模式下的线程同步的扩展,因为用户模式下的线程同步有一定的局限性.但用户模式下线程同步的好处是速度快,不需要切换到内核模式(需要额外的 CPU 时间).通 ...

  6. 第8章 用户模式下的线程同步(4)_条件变量(Condition Variable)

    8.6 条件变量(Condition Variables)——可利用临界区或SRWLock锁来实现 8.6.1 条件变量的使用 (1)条件变量机制就是为了简化 “生产者-消费者”问题而设计的一种线程同 ...

  7. 第8章 用户模式下的线程同步(1)_Interlocked系列函数

    8.1 原子访问:Interlocked系列函数(Interlock英文为互锁的意思) (1)原子访问的原理 ①原子访问:指的是一线程在访问某个资源的同时,能够保证没有其他线程会在同一时刻访问该资源. ...

  8. 第8章 用户模式下的线程同步(3)_Slim读写锁(SRWLock)

    8.5 Slim读/写锁(SRWLock)——轻量级的读写锁 (1)SRWLock锁的目的 ①允许读者线程同一时刻访问共享资源(因为不存在破坏数据的风险) ②写者线程应独占资源的访问权,任何其他线程( ...

  9. 第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

    8.4 关键段(临界区)——内部也是使用Interlocked函数来实现的! 8.4.1 关键段的细节 (1)CRITICAL_SECTION的使用方法 ①CRITICAL_SECTION cs;   ...

随机推荐

  1. three.js cannon.js物理引擎之齿轮动画

    郭先生今天继续说一说cannon.js物理引擎,并用之前已经学习过的知识实现一个小动画,知识点包括ConvexPolyhedron多边形.Shape几何体.Body刚体.HingeConstraint ...

  2. 记录 Allsec 解题过程

    开局打开URL:http://119.3.191.245:65532/#/allsecPlayGame,前去做游戏 游戏URL:http://119.3.191.245:8877/Login.php ...

  3. gulp打包详解

    gulp的作用 删除文件中冗余的内容,压缩文件,减小文件体积 实际项目中运行的都是压缩完成以后的文件 减小加载响应时间 gulp打包压缩对象 html,css,js,sass,webserver 音频 ...

  4. $.ajax data向后台传递参数失败 contentType: "application/json"

    在ajax方法设置中若不添加 contentType: "application/json" 则data可以是对象: $.ajax({ url: actionurl, type: ...

  5. 轻量易用的微信Sdk发布——Magicodes.Wx.Sdk

    概述 最简洁最易于使用的微信Sdk,包括公众号Sdk.小程序Sdk.企业微信Sdk等,以及Abp VNext集成. GitHub地址:https://github.com/xin-lai/Magico ...

  6. 3、MyBatis教程之CURD操作

    4.CURD操作 1.查询 根据用户 Id查询用户 在UserMapper中添加对应方法 public interface UserMapper { List<User> getUserL ...

  7. Git 上传项目到 Github

    Git 上传项目到 Github 该文章主要讲解Git 上传项目到 Github,Gitee同理 配置Git 下载.安装Git 下载后一路(傻瓜式安装)直接安装即可 如果第一次使用git的话,需要设置 ...

  8. Redis实战篇(三)基于HyperLogLog实现UV统计功能

    如果现在要开发一个功能: 统计APP或网页的一个页面,每天有多少用户点击进入的次数.同一个用户的反复点击进入记为 1 次,也就是统计 UV 数据. 让你来开发这个统计模块,你会如何实现? 如果统计 P ...

  9. Flutter原理简介

    Flutter 是怎么运转的? 与用于构建移动应用程序的其他大多数框架不同,Flutter 是重写了一整套包括底层渲染逻辑和上层开发语言的完整解决方案.这样不仅可以保证视图渲染在 Android 和 ...

  10. 前端 | JS Promise:axios 请求结果后面的 .then() 是什么意思?

    Promise 是JS中一种处理异步操作的机制,在现在的前端代码中使用频率很高.Promise 这个词可能有点眼生,但你肯定见过 axios.get(...).then(res => {...} ...