内核模式下的线程同步

内核模式下的线程同步是用户模式下的线程同步的扩展,因为用户模式下的线程同步有一定的局限性。但用户模式下线程同步的好处是速度快,不需要切换到内核模式(需要额外的 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. 番外----python入门----pip相关

    pip 是 Python 包管理工具,该工具提供了对Python 包的查找.下载.安装.卸载的功能. 但是,由于pip使用的pip仓库默认为:http://pypi.python.org/ 是国外的 ...

  2. php小结

    //函数定义常量:definedefine("PI",3.14); //使用const关键字const PII = 3; 特殊常量:双下划线开始+常量名+双下划线结束,称为魔术常量 ...

  3. 07、列表list

    列表(list) 是一个有序且可变的容器,在里面可以存放多个不同类型的元素 list = ['阿斯顿','阿发师','收发室'] list = [98,88,66,-1] list = [1,True ...

  4. vue 给一个值重置初始值

    查了下资料很多都是如下: 1. this.$options.data() 这个可以获取原始的data值,this.$data 获取当前状态下的data,拷贝重新赋值一下就行了. 1 Object.as ...

  5. 面向对象进阶时,if语句写错位置

    这周blog我也不知道要写什么,因为这章我其实学得有点懵,前面那几天我纠结了好久代码,一直不知道原因错在哪里.后来经过询问老师才知道自己调用错了构造方法,相信也有跟我一样的新手会犯这个错误.我在创建关 ...

  6. python登陆界面尝试

    示例1: """ 编写一个程序 用户可以输入用户名和密码 用户有三次机会 登录成功可以进行相应的操作 输入Q退出系统 """ name_li ...

  7. java例题_16 九九乘法表

    1 /*题目:输出 9*9 口诀. 2 程序分析:分行与列考虑,共 9 行 9 列,i 控制行,j 控制列. 3 */ 4 5 /*分析 6 * 用两侧for循环,外层循环还要控制换行 7 * 换行时 ...

  8. 再谈synchronized锁升级

    在图文详解Java对象内存布局这篇文章中,在研究对象头时我们了解了synchronized锁升级的过程,由于篇幅有限,对锁升级的过程介绍的比较简略,本文在上一篇的基础上,来详细研究一下锁升级的过程以及 ...

  9. 它来了,它来了,HarmonyOS应用开发在线体验来了

    接下来是我们的两分钟科普,一分钟玩转HarmonyOS应用开发在线体验,一分钟简单了解"一次开发.多设备部署"的原理.萌新的开发者也能第一时间掌握,往下看吧~ 一分钟玩转Harmo ...

  10. Day14_75_反射(reflect)

    反射 反射的基本概念 反射是由Smith在1982年首次提出,主要是指程序可以访问,检测,修改它本身状态或行为的一种能力.并且能够根据自身行为的状态或结果,调整和修改所描述行为的状态和相关语义. ja ...