3.线程等待与唤醒

xiaoxiao2025-11-14  6

我们在之前的讲解了如何自己实现临界区以及什么是Windows自旋锁,这两种同步方案在线程无法进入临界区时都会让当前线程进入等待状态,一种是通过Sleep函数实现的,一种是通过让当前的CPU"空转”实现的,但这两种等待方式都有局限性:

通过Sleep函数进行等待,等待时间该如何确定呢?通过“空转”的方式进行等待,只有等待时间很短的情况下才有意义,否则对CPU资源是种浪费。而且自旋锁只能在多核的环境下才有意义。

有没有更加合理的等待方式呢?只有在条件成熟的时候才将当前线程唤醒?

等待与唤醒机制

在Windows中,一个线程可以通过等待一个或者多个可等待对象,从而进入等待状态,另一个线程可以在某些时刻唤醒等待这些对象的其他线程。

A线程用WaitForSingleObject 或者 WaitForMultipleObjects进入等待状态,B线程用SetEvent将线程唤醒,A线程与B线程就是通过 可等待对象 联系到一起的。

可等待对象

在Windbg中查看如下结构体:

进程 dt KPROCESS 线程 dt KTHREAD 定时器 dt_ KTIMER 信号量 dt KSEMAPHOREI 事件 dt KEVENT 互斥体 dt KMUTANT 文件 dt FILE OBJECT

可等待对象都有一个共同的特点,他们的第一个成员都是一个结构体:_DISPATCHER_HEADER。但是有一些结构体并不是比如说文件就不是。 只要结构体中有_DISPATCHER_HEADER就是可等待对象。

kd> dt _DISPATCHER_HEADER nt!_DISPATCHER_HEADER +0x000 Type //该对象类型 +0x001 Absolute +0x002 Size +0x003 Inserted +0x004 SignalState //该分发器信号状态 (值大于0就是有信号 分发器对象也称为同步对象) +0x008 WaitListHead //双向链表头,链着所有等待块(此链表包含了所有正在等待该分发器对象的线程)

可等待对象的差异

WaitForSingleObject(3) NtWaitorSingleObject(内核) 1)通过3环用户提供的句柄,找到等待对象的内核地址。 2)如果是以_DISPATCHER-HEADER开头,直接使用。 3)如果不是以DISPATCHER HEADER开头的对象,则找到在其中嵌入的_DISPATCHER_HEADER对象。 KeWaitForSingleObject(内核) -------核心功能,后面在将

测试代码

#include <stdio.h> #include<windows.h> HANDLE hEvent[2]; VOID WINAPI ThreadProc(LPVOID text) { ::WaitForSingleObject(hEvent[0], -1); printf("ThreadProc函数执行...\n"); } int main() { hEvent[0] = ::CreateEvent(NULL, TRUE, FALSE, NULL); ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL); getchar(); return 0; }

windbg中可以通过kd> !process 892ec020 来查看该进程中所有的线程。

还可以通过查看等待链表来找到该线程,使用了WaitForSingleObject此线程就会出现在等待链表中。

(kd> dt _KTHREAD 895458b8) WaitBlockList 等待块,当前线程就是等待块与被等待块联系到一起的。

等待块中的每个成员的值

kd> dt _KWAIT_BLOCK 0x89545928 nt!_KWAIT_BLOCK +0x000 WaitListEntry : _LIST_ENTRY [ 0x89320290 - 0x89320290 ] +0x008 Thread : 0x895458b8 _KTHREAD //当前线程 +0x00c Object : 0x89320288 Void //被等待对象的地址 +0x010 NextWaitBlock : 0x89545928 _KWAIT_BLOCK /*单向循环链表, 如果当前线程只有一个等待对象它指向自己, 多个等待对象就是第一个指向第二个,最后一个指向第一个*/ +0x014 WaitKey : 0 //当等待块索引,我这是第0个 +0x016 WaitType : 1 /*等待时要求只要有一个等待对象符合条件就可以被激活那么它的值就是1, 如果如果你等待多个对象必须全部符合条件才可以被激活它就是0*/



下面来看看一个线程等待多个对象的情况。

#include <stdio.h> #include<windows.h> HANDLE hEvent[2]; VOID WINAPI ThreadProc(LPVOID text) { ::WaitForMultipleObjects(2, hEvent, FALSE, -1); printf("ThreadProc函数执行...\n"); } int main() { DWORD thread_id_1, thread_id_2; hEvent[0] = ::CreateEvent(NULL, TRUE, FALSE, NULL); hEvent[1] = ::CreateEvent(NULL, TRUE, FALSE, NULL); ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, (LPDWORD)&thread_id_1); getchar(); return 0; }

windbg中查看情况

+0xc Object指向被等待对象。

被等待对象的 WaitListHead 是一个双向链表,这个链表链链着所有等待块。

比如说你当前等待的这个对象可能也被别的线程等待着,WaitListHead就把等待它的所有线程的_KWAIT_BLOCK 穿到他的第一个成员中( +0x000 WaitListEntry)。

WaitListHead是链表头它链着所有等待块,挂的位置就是_KWAIT_BLOCK .WaitListEntry。



等待网 如图:线程1正在等待分发器对象B,线程2正在等待分发器对象A和B。

当对象B变成有信号状态时,线程1的等待条件已满足,于是他变成延迟的就绪状态,以便被调度和执行; 而线程2的等待条件是否满足,要决取于它的等待类型,KWAIT_BLOCK中的WaitType记录了线程的等待类型,若是WaitAny则线程2的等待条件满足;若是WaitAll线程2继续等待。

当对象A变成有信号状态时,线程2的等待条件满足,于是他变成延迟的就绪状态,以便被调度和执行。

总结:

等待中的线程,一定在等待链表中(KiWaitListHead),同时也一定在这张网上 (KTHREAD +5C的位置不为空)。线程通过调用WaitForSingleObject/WaitForMultipleObjects函数将自己挂到这 ,张网上。线程什么时候会再次执行取决于其他线程何时调用相关函数,等待对象不同调用的函数也不同。
转载请注明原文地址: https://www.6miu.com/read-5039607.html

最新回复(0)