内核模式下的线程同步

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

    线程安全问题是一个老生常谈的问题,那么多线程环境下究竟有那些问题呢?这么说吧,问题的形式多种多样的,归根结底的说是共享资源问题,无非可见性与有序性问题. 1. 可见性 可见性是对于内存中的共享资源来说 ...

  2. Mysql将查询结果某个字段以逗号分隔,使用group_concat函数可以实现(配合group by使用)

    示例:SELECT Id, GROUP_CONCAT(Name SEPARATOR ',') Names FROM some_table GROUP BY id

  3. python基础学习之描述符和装饰器

    描述符的了解: 描述符协议: python描述符是一个"绑定行为"的对象属性,在描述符协议中,它可以通过方法重写属性的访问.这些方法有: __get__, __set__, 和__ ...

  4. 攻防世界 reverse Newbie_calculations

    Newbie_calculations Hack-you-2014 题目名百度翻译成新手计算,那我猜应该是个实现计算器的题目.... IDA打开程序,发现一长串的函数反复调用,而且程序没有输入,只有输 ...

  5. pwnable.kr第三题bof

    Running at : nc pwnable.kr 9000 IDA查看 1 unsigned int __cdecl func(int a1) 2 { 3 char s; // [esp+1Ch] ...

  6. golang 性能调优分析工具 pprof(下)

    golang 性能调优分析工具 pprof(上)篇, 这是下篇. 四.net/http/pprof 4.1 代码例子 1 go version go1.13.9 把上面的程序例子稍微改动下,命名为 d ...

  7. 痞子衡嵌入式:MCUXpresso IDE下在线调试时使用不同复位策略的现象总结

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是MCUXpresso IDE下在线调试时使用不同复位策略的现象总结. 本篇实际上是<IAR在线调试时设不同复位类型可能会导致i.M ...

  8. [图论]最短网络:prim

    最短网络 目录 最短网络 Description Input Output Sample Input Sample Output 解析 代码 Description 农民约翰被选为他们镇的镇长!他其中 ...

  9. 【Django学习笔记】-环境搭建

    对于初学django新手,根据以下步骤可以快速进行Django环境搭建 虚拟环境创建 使用virtualenv创建并启用虚拟机环境 ,关于virtualenv可参考https://www.yuque. ...

  10. Dynamics CRM安装教程四:DNS配置

    在为MS CRM 配置Claims-based认证之前,你需要在域控服务器的DNS中添加一些记录,来解析CRM的各个断点,添加內容如下(本次环境全部安装在一台机子中): AD FS 服务器(例: ad ...