内核模式下的线程同步的分析(Windows核心编程)
内核模式下的线程同步
内核模式下的线程同步是用户模式下的线程同步的扩展,因为用户模式下的线程同步有一定的局限性。但用户模式下线程同步的好处是速度快,不需要切换到内核模式(需要额外的 CPU 时间)。通常情况下可以使用三种内核对象对线程进行同步,分别是事件内核对象、信号量内核对象和互斥量内核对象
注:不论是事件内核对象、信号量内核对象还是互斥量内核对象都遵循着触发与未触发的原则。对于进程和线程来说未触发表明线程或进程处于等待状态或者挂起状态(CPU 此时还没有进行调度),而触发表明进程或线程处于运行结束的状态
0x01 事件内核对象
- 事件内核对象相比于其他用于进行线程同步的内核对象要简单的多,下面就是操作事件内核对象的一些函数:
(1) CreateEvent:创建一个事件内核对象 (2) OpenEvent:打开一个事件内核对象 (3) SetEvent:将一个事件内核对象变为触发状态 (4) ResetEvent:将一个事件内核对象变为未触发状态
- 例子一:
#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 score = 0;
// 创建事件内核对象
HANDLE Event;
int main(int argc, char *argv[])
{
ThreadCommunication();
return 0;
}
DWORD WINAPI ThreadCommunication()
{
// 创建事件内核对象,并且将第二个参数传入 TRUE,表示自动重置,也就是说有等待成功的副作用
Event = CreateEvent(NULL, TRUE, FALSE, TEXT("EventObj"));
// 创建线程
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);
// 设置事件对象为触发状态
SetEvent(Event);
// 等待线程退出
HANDLE Threads[2] = { 0 }; Threads[0] = myThread1; Threads[1] = myThread2;
WaitForMultipleObjects(2, Threads, TRUE, INFINITE);
// 关闭线程句柄
CloseHandle(myThread1); CloseHandle(myThread2); CloseHandle(Event);
// 最后打印共享资源
cout << "[*] score: " << score << endl;
return TRUE;
}
// 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
WaitForSingleObject(Event, INFINITE);
score++;
Sleep(700);
cout << "线程一" << endl;
// 重新设置事件对象为触发状态
SetEvent(Event);
return TRUE;
}
// 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
WaitForSingleObject(Event, INFINITE);
Sleep(700);
cout << "线程二" << endl;
score++;
// 重新设置事件对象为触发状态
SetEvent(Event);
return TRUE;
}
- 为了实现线程同步的访问共享资源,首先创建了一个处于未触发状态下的事件内核对象 EventObj ,然后创建了两个线程等待事件内核对象被触发,之后使用 SetEvent 函数将事件内核对象变为触发状态,这样两个线程同时访问共享资源就不会发生冲突的情况
- 从表面看事件内核对象解决了访问共享资源冲突的问题,但是细心一看发现有一个问题,两个函数都会使用 WaitForSingleObject 函数等待 EventObj 这个内核对象。假设这个事件内核对象被 SetEvent 函数变为触发状态时,线程一等待成功了,于是 CPU 开始执行线程一的代码,之后线程二也等待成功了发现这个事件内核对象还是触发状态,于是 CPU 又开始执行线程二的代码,那还是会访问冲突啊。但实际上并非如此,原因就在于 WaitForSingleObject 这个函数有等待成功所引起的副作用,自动的将事件内核对象变为未触发的状态,这样不论线程一还是线程二的 WaitForSingleObject 函数等待成功都会将事件内核对象变为未触发状态,这样另外一个线程就会继续等待事件内核对象变为触发状态,之后只需要在线程结尾动用 SetEvent 函数重新将事件内核对象变为触发状态即可,这样的话被等待的线程就又可以运行了
注:并不是所有的内核对象被 WaitForSingleObject 等函数等待之后都会有等待成功所引起的副作用,比如线程和进程内核对象被等待就不会有等待成功所引起的副作用
0x02 信号量内核对象
- 信号量内核对象主要作用是对资源进行计数,操作信号量内核对象的函数:
(1) CreateSemaphore(Ex):创建一个信号量内核对象 (2) OpenSemaphore:打开一个信号量内核对象 (3) ReleaseSemaphore:增加信号量的资源计数
- 举个例子:
#include <Windows.h>
#include <iostream>
#include <process.h>
using namespace std;
DWORD WINAPI ThreadCommunication();
unsigned __stdcall ThreadFun(void* pvParam);
int main(int argc, char *argv[])
{
ThreadCommunication();
return 0;
}
// 信号量内核对象
HANDLE Semaphore;
// 线程计数
long ThreadCount = 0;
DWORD WINAPI ThreadCommunication()
{
// 创建信号量内核对象,第二个参数表示一开始有 5 个资源可以使用,第三个参数表示最大资源数为 5
Semaphore = CreateSemaphore(NULL, 5, 5, NULL);
// 创建线程池
HANDLE Threads[50];
// 创建 50 个挂起线程
for (size_t i = 0; i < 50; i++)
{
Threads[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, CREATE_SUSPENDED, NULL);
cout << "线程 " << i + 1 << " 开始运行" << endl;
}
// 运行它们
for (size_t i = 0; i < 50; i++)
{
ResumeThread(Threads[i]);
}
// 等待 50 个线程结束
WaitForMultipleObjects(50, Threads, TRUE, INFINITE);
// 关闭线程句柄
for (size_t i = 0; i < 50; i++)
{
CloseHandle(Threads[i]);
}
// 打印 ThreadCount 的值
cout << "ThreadCount 的值为: " << ThreadCount << endl;
return TRUE;
}
unsigned __stdcall ThreadFun(void* pvParam)
{
// 等待信号量内核对象触发
WaitForSingleObject(Semaphore, INFINITE);
cout << "线程开始" << endl;
Sleep(300);
// 实现对共享资源的访问
InterlockedIncrement(&ThreadCount);
// 将信号量内核对象加一
ReleaseSemaphore(Semaphore, 1, NULL);
return TRUE;
}
- 上面的程序首先创建了 50 个线程并且运行它们,然后创建了一个初始资源数为 5 且最大资源数为 5 信号量内核对象(这样就可以控制线程最大并发量为 5,超过 5 个线程的话就需要进行等待),最后在线程内部使用 WaitForSingleObject 函数等待信号量,线程运行完成之后在结尾使用 ReleaseSemaphore 函数将信号量内核对象加一
注:因为创建的信号量内核对象一开始就会有 5 个资源可供使用,而 WaitForSingleObject 函数具有等待成功引起的副作用(会将资源数减一),所以一开始会有 5 个线程将抢到这 5 个资源,那么资源计数就会变为 0(0 表示未触发状态),所以其他线程只能等待;等到 5 个线程完成了任务之后,又会将信号量加一,这样资源数又会变为 5,如此往复循环,就控制了线程最大并发量
0x03 互斥量内核对象
- 相比于事件内核对象和信号量内核对象,互斥量则比较特殊,因为功能没有它们强大,主要用于确保线程独占一个资源的访问,所以和关键段或者读写锁很相似。用于操作互斥量内核对象的函数:
(1) CreateMutex:创建一个互斥量内核对象 (2) OpenMutex:打开一个互斥量内核对象 (3) ReleaseMutex:将互斥量内核对象递归计数减一
- 这样就可以将上面的信号量内核对象程序修改为下面这样:
#include <Windows.h>
#include <iostream>
#include <process.h>
using namespace std;
DWORD WINAPI ThreadCommunication();
unsigned __stdcall ThreadFun(void* pvParam);
int main(int argc, char *argv[])
{
ThreadCommunication();
return 0;
}
// 信号量内核对象
HANDLE Semaphore;
// 互斥量内核对象
HANDLE Mutex;
// 线程计数
long ThreadCount = 0;
DWORD WINAPI ThreadCommunication()
{
// 创建信号量内核对象,第二个参数表示一开始有 5 个资源可以使用,第三个参数表示最大资源数为 5
Semaphore = CreateSemaphore(NULL, 5, 5, NULL);
// 创建互斥量内核对象,TRUE 表示未触发(引用计数为 1),FALSE 表示触发(引用计数为 0)
Mutex = CreateMutex(NULL, FALSE, NULL);
// 创建线程池
HANDLE Threads[50];
// 创建 50 个挂起线程
for (size_t i = 0; i < 50; i++)
{
Threads[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, CREATE_SUSPENDED, NULL);
cout << "线程 " << i + 1 << " 开始运行" << endl;
}
// 运行它们
for (size_t i = 0; i < 50; i++)
{
ResumeThread(Threads[i]);
}
// 等待 50 个线程结束
WaitForMultipleObjects(50, Threads, TRUE, INFINITE);
// 关闭线程句柄
for (size_t i = 0; i < 50; i++)
{
CloseHandle(Threads[i]);
}
// 打印 ThreadCount 的值
cout << "ThreadCount 的值为: " << ThreadCount << endl;
return TRUE;
}
unsigned __stdcall ThreadFun(void* pvParam)
{
// 等待信号量内核对象触发
WaitForSingleObject(Semaphore, INFINITE);
cout << "线程开始" << endl;
Sleep(300);
// 实现对共享资源的访问
WaitForSingleObject(Mutex, INFINITE);
ThreadCount++;
// 将互斥量计数减一变为触发状态
ReleaseMutex(Mutex);
//InterlockedIncrement(&ThreadCount);
// 将信号量内核对象加一
ReleaseSemaphore(Semaphore, 1, NULL);
return TRUE;
}
- 这样就不需要使用 InterlockedIncrement 函数来以原子方式来更新共享资源,用互斥量就可以
注:值得注意的是,互斥量的计数只有 0 和 1 两种状态(而信号量可以为正整数)并且 0 表示触发,而 1 表示未触发;除了这些区别互斥体还有遗弃等等问题,正常使用中注意就行
0x04 内核对象线程同步和等待成功的副作用参考表
- 对象 - | - 何时处于未触发状态 - | - 何时处于触发状态 - | - 等待成功副作用 - |
进程 | 进程正在运行的时候 | 进程终止的时候 | 没有 |
线程 | 线程正在运行的时候 | 线程终止的时候 | 没有 |
作业 | 作业尚未超时的时候 | 作业超时的时候 | 没有 |
文件 | 有待处理 I/O 请求的时候 | I/O 请求完成的时候 | 没有 |
控制台输入 | 没有输入的时候 | 有输入的时候 | 没有 |
文件变更通知 | 文件没有变更的时候 | 文件系统检测到变更的时候 | 重置通知 |
自动重置事件(Event) | ResetEvent 函数设置为未触发 | SetEvent 函数设置为触发 | 重置事件 |
手动重置事件 | 同上 | 同上 | 没有 |
自动重置可等待计时器 | CancelWaitableTimer 或等待成功 | 时间到的时候 | 重置计时器 |
手动重置可等待计时器 | 同上 | 同上 | 没有 |
信号量 | 等待成功的时候 | 计数大于 0 的时候 | 计数减一 |
互斥量 | 等待成功的时候 | 不为线程占用的时候 | 把所有权交给线程 |
关键段 | 同上 | 同上 | 同上 |
SRWLock | 同上 | 同上 | 同上 |
条件变量 | 等待成功的时候 | 被唤醒的时候 | 没有 |
内核模式下的线程同步的分析(Windows核心编程)的更多相关文章
- 用户模式下的线程同步的分析(Windows核心编程)
线程同步 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源 ...
- windows核心编程---第七章 用户模式下的线程同步
用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...
- 《windows核心编程系列》七谈谈用户模式下的线程同步
用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...
- 【windows核心编程】 第八章 用户模式下的线程同步
Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ① 需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ② 一个线程需要通知其他线程 ...
- Windows核心编程:第8章 用户模式下的线程同步
Github https://github.com/gongluck/Windows-Core-Program.git //第8章 用户模式下的线程同步.cpp: 定义应用程序的入口点. // #in ...
- 第8章 用户模式下的线程同步(4)_条件变量(Condition Variable)
8.6 条件变量(Condition Variables)——可利用临界区或SRWLock锁来实现 8.6.1 条件变量的使用 (1)条件变量机制就是为了简化 “生产者-消费者”问题而设计的一种线程同 ...
- 第8章 用户模式下的线程同步(1)_Interlocked系列函数
8.1 原子访问:Interlocked系列函数(Interlock英文为互锁的意思) (1)原子访问的原理 ①原子访问:指的是一线程在访问某个资源的同时,能够保证没有其他线程会在同一时刻访问该资源. ...
- 第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)
8.4 关键段(临界区)——内部也是使用Interlocked函数来实现的! 8.4.1 关键段的细节 (1)CRITICAL_SECTION的使用方法 ①CRITICAL_SECTION cs; ...
- 第8章 用户模式下的线程同步(3)_Slim读写锁(SRWLock)
8.5 Slim读/写锁(SRWLock)——轻量级的读写锁 (1)SRWLock锁的目的 ①允许读者线程同一时刻访问共享资源(因为不存在破坏数据的风险) ②写者线程应独占资源的访问权,任何其他线程( ...
随机推荐
- MySQL使用入门--初识数据库
MySQL使用入门 数据库概述 数据库是存放数据的仓库.在应用的开发中总是离不开数据的查询.处理.存储,例如图书管理系统就需要操纵和存储大量的数据.没有数据库之前我们使用文件存储数据,但是文件存储有很 ...
- [数据结构与算法-15]单源最短路径(Dijkstra+SPFA)
单源最短路径 问题描述 分别求出从起点到其他所有点的最短路径,这次主要介绍两种算法,Dijkstra和SPFA.若无负权优先Dijkstra算法,存在负权选择SPFA算法. Dijkstra算法 非负 ...
- JavaScript初级学习
1. JavaScript的介绍 前身是LiveScript+JavaScript JavaScript(js)是一个脚本语言 基于浏览器的脚本语言 基于对象,面向对象的一个编程语言 2. EcmaS ...
- 漏洞复现-fastjson1.2.24-RCE
0x00 实验环境 攻击机:Win 10.Win Server2012 R2(公网环境,恶意java文件所在服务器) 靶机也可作为攻击机:Ubuntu18 (公网环境,docker ...
- 《Asp.Net Core3 + Vue3入坑教程》 - Vue 1.使用vue-cli创建vue项目
简介 <Asp.Net Core3 + Vue3入坑教程> 此教程适合新手入门或者前后端分离尝试者.可以根据图文一步一步进操作编码也可以选择直接查看源码.每一篇文章都有对应的源码 目录 & ...
- 爬虫必知必会(3)_requests模块高级
一.爬虫爬取失败的几个原因 1.在短时间内向网站发起了一个高频的请求 解决办法:使用代理 2.连接池(http)中的资源被耗尽 解决办法:立即将请求断开:Connection:close 3.高清图片 ...
- 一个通用驱动Makefile-V2-支持编译多目录
目录 前言 1. 特点 2. 分析 2.1 简要原理 2.2 具体分析 3. 源码 前言 该 Makefile 已经通过基于内核 Linux5.4 版本验证通过. 因为编写这通用驱动 Makefile ...
- CRC校验原理和verilog实现方法(一)
1.CRC简介 CRC全称循环冗余校验(Cyclic Redundancy Check, CRC),是通信领域数据传输技术中常用的检错方法,用于保证数据传输的可靠性.网上有关这方面的博客和资料很多,本 ...
- JVM之基础概念(运行时数据区域、TLAB、逃逸分析、分层编译)
运行时数据区域 JDK8 之前的内存布局 JDK8 之后的 JVM 内存布局 JDK8 之前,Hotspot 中方法区的实现是永久代(Perm),JDK8 开始使用元空间(Metaspace),以前永 ...
- Java8中的默认方法
作者:汤圆 个人博客:javalover.cc 前言 大家好啊,我是汤圆,今天给大家带来的是<Java8中的默认方法>,希望对大家有帮助,谢谢 文章纯属原创,个人总结难免有差错,如果有,麻 ...