内核模式下的线程同步

内核模式下的线程同步是用户模式下的线程同步的扩展,因为用户模式下的线程同步有一定的局限性。但用户模式下线程同步的好处是速度快,不需要切换到内核模式(需要额外的 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核心编程)的更多相关文章

  1. 用户模式下的线程同步的分析(Windows核心编程)

    线程同步 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源 ...

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

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

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

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

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

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

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

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

  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章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

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

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

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

随机推荐

  1. redis基础:redis下载安装与配置,redis数据类型使用,redis常用指令,jedis使用,RDB和AOF持久化

    知识点梳理 课堂讲义 课程计划 1. REDIS 入 门 (了解) (操作)   2. 数据类型 (重点) (操作) (理解) 3. 常用指令   (操作)   4. Jedis (重点) (操作) ...

  2. MySQL 多表查询与事务的操作

    表连接查询 什么是多表查询 # 数据准备 # 多表查询的作用 * 比如:我们想查询孙悟空的名字和他所在的部门的名字,则需要使用多表查询 # 如果一条 SQL 语句查询多张表,因为查询结果在多张不同的表 ...

  3. MVC模式从Controller返回内容协商格式(Json或者Xml)

    WebAPI默认的返回格式Json,但是MVC是View,如果在MVC的控制器中,想要返回Json格式该怎么操作呢 在MVC的控制器中返回json数据只需要然会JsonResult而不是ActionR ...

  4. LAB1 启动操作系统

    从机器上电到运行OS发生了什么? 在电脑主板上有一个Flash块,存放了BIOS的可执行代码.它是ROM,断电不会丢掉数据.在机器上电的时候,CPU要求内存控制器从0地址读取数据(程序第一条指令)的时 ...

  5. 如何强制删除一个apk

    有些apk安装完后无法卸载,现在收集了一些方法,以及个人的一些手段. 1. 假设该app名为ketech,安装包名为ketech.apk. 2. 查看/data/app里面是否有名称包含ketech的 ...

  6. Educational Codeforces Round 64 C. Match Points 【二分思想】

    一 题面 C. Match Points 二 分析 根据题意很容易想到要去找满足条件的数,因为可以打乱输入的顺序,所以很容易想到二分. 但是如果直接对输入的数组进行二分,如输入$a$,直接在数组里二分 ...

  7. JWT加密解密方法

    public static string Key { get; set; } = "123456789987654321";//解密串 /// <summary> // ...

  8. c# DataGirdView动态刷新

    using MySql.Data.MySqlClient;using System; using System.Data; using System.Threading; using System.W ...

  9. 使用ant design vue的日历组件,实现一个简单交易日与非交易日的切换

    使用ant design vue的日历组件,实现一个简单交易日与非交易日的切换 需求: 日历区分交易日.非交易日 可以切换面板查看整年交易日信息 可以在手动调整交易日.非交易日 演示实例 序--使用软 ...

  10. 以绝对优势立足:从CDN和云存储来聊聊云生态的崛起

    以绝对优势立足:从CDN和云存储来聊聊云生态的崛起 前面几期文章我们介绍了混合云模式,以及面向应用层的云架构解决方案的Spring Cloud.接下来,我们就以蘑菇街的两个具体案例,来分享一下基于混合 ...