内核模式下的线程同步的分析(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锁的目的 ①允许读者线程同一时刻访问共享资源(因为不存在破坏数据的风险) ②写者线程应独占资源的访问权,任何其他线程( ...
随机推荐
- 任务队列 与 Celery概述
一.任务队列(Task Queues) 1.1 什么是任务队列? 任务队列用于管理后台工作,通常这些后台工作必须在 HTTP请求-响应循环 之外执行. 1.2 为什么需要任务队列? 对于那些不是由客户 ...
- mysql创建读写账号及服务相关优化配置
grant select on xoms_prod.* to 'kzcf'@'%' identified by '123456'; 赋权多权限就 grant select,update,delet ...
- 如何提高Web应用系统的性能?
随着互联网信息技术的发展,人们逐渐开始习惯在网络上交友.购物.学习.娱乐.工作,甚至是找工作.因此市场对网站的响应速度也提出了新的要求,提高Web应用系统的性能成为急需解决的关键问题.本文将会给出一些 ...
- Java进阶专题(二十七) 将近2万字的Dubbo原理解析,彻底搞懂dubbo (下)
...接上文 服务发现 服务发现流程 整体duubo的服务消费原理 Dubbo 框架做服务消费也分为两大部分 , 第一步通过持有远程服务实例生成Invoker,这个Invoker 在客户端是核心的远程 ...
- java例题_46 两个字符串拼接问题!
1 /*46 [程序 46 字符串连接] 2 题目:两个字符串连接程序,将两个字符串拼接在一起 3 */ 4 5 /*分析 6 * 两个字符串的拼接方法 7 * concat方式 8 * 当两个量都为 ...
- 面试官:聊一聊SpringBoot服务监控机制
目录 前言 SpringBoot 监控 HTTP Endpoints 监控 内置端点 health 端点 loggers 端点 metrics 端点 自定义监控端点 自定义监控端点常用注解 来,一起写 ...
- 基于k8s的集群稳定架构-转载
基于k8s的集群稳定架构-转载 前言 我司的集群时刻处于崩溃的边缘,通过近三个月的掌握,发现我司的集群不稳定的原因有以下几点: 1.发版流程不稳定 2.缺少监控平台[最重要的原因] 3.缺少日志系统 ...
- DDOS攻击与防御简单阐述,列出DDOS的攻击方法和防御方法
参考1:https://www.hi-linux.com/posts/50873.html#%E7%BD%91%E7%BB%9C%E5%B1%82-ddos-%E6%94%BB%E5%87%BB 什么 ...
- Simulink中Scope数据保存至Workspace制图
0 问题 通常情况下,仿真模型中scope波形可编辑程度并不高,尽管高版本MATLAB中已经可以将其直接导出到figure,但效果并不是特别理想.在需要高质量输出波形图场合,就需要将其中数据导出到wo ...
- WPF -- 使用当前进程打开自定义文件的一种方式
问题描述 当双击打开自定义格式的文件时,希望使用当前正在运行的进程,而不是另起一个进程. 本文介绍一种方式解决如上问题,方案参考user3582780的解答 设置自定义文件格式的默认打开方式 参考链接 ...