本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。
用内核对象进行线程同步
内核对象:Windows操作系统使用内核对象来管理进程、线程、文件等诸多种类的大量资源。内核对象的创建通常是通过Windows API,比如CreateThread将创建一个线程内核对象,并返回一个内核对象句柄。内核对象实际上是一小块内存,其中包括了引用计数、安全性描述等信息,操作系统通过这一小段内存来管理对应的内核资源。内核对象的实际内存地址并非句柄所展示的,它们在进程内的内核对象句柄表中有映射。
在前几篇中,介绍了在用户模式下的线程同步机制:InterLocked系列、关键段、Slim读写锁。这些同步机制可以在进行线程同步的同时让线程保持在用户模式下。然而用户模式下的线程同步机制有时不能满足我们的要求。从这篇开始,将介绍使用内核对象进行线程同步。在考虑是使用用户模式的同步机制还是使用内核对象来同步的时候,需要综合考量,尽量使用用户模式的线程同步机制。
内核对象普遍存在两种状态,要么是触发,要么是未触发。每种内核对象在这两个状态间切换过程都有其特殊的特点,比如进程内核对象在创建的时候总是处于未触发状态,当进程终止时,会变成触发状态;而且进程内核对象永远不会从触发态变回未触发态。于是,如果我们的线程需要等待子进程终止时才继续,那么可以将线程进入等待状态,当子进程标识的进程内核对象变成触发状态的时候唤醒线程!我们所需要的仅仅是Windows为我们提供的等待函数。
等待函数
等待函数能够是一个线程进入等待状态,直到指定的内核对象被触发为止。这些等待函数有:
1 2 3 4 | DWORD WINAPI WaitForSingleObject( __in HANDLE hHandle, __in DWORD dwMilliseconds ); |
当线程调用WaitForSingleObject的时候,第一个参数hObject用来标识内核对象,第二个参数是个超时时间(传入INFINITE表示永远等待直到触发)。
1 2 3 4 5 6 | DWORD WINAPI WaitForMultipleObjects( __in DWORD nCount, __in const HANDLE *lpHandles, __in BOOL bWaitAll, __in DWORD dwMilliseconds ); |
WaitForMultipleObjects允许调用线程检查多个内核对象的触发状态。
使用等待函数有时是会改变内核对象的状态的。比如:线程正在等待一个自动重置事件对象,当事件对象被触发的时候,函数会检测到这一情况并返回调用线程,但是在返回之前,他会使事件变为非触发状态。等待函数在不同的内核对象上调用并返回所引起的这样类似的“副作用”是不同的。在对内核对象展开介绍后,将看到这一点。
如果多个线程在同时等待同一个内核对象,那么任何一个线程都有可能被唤醒。我们不应该做出类似“谁先等待谁就先唤醒”这样的假设。
接下来,将分几篇的内容分别介绍那些与线程同步有关的内核对象。
事件内核对象
事件内核对象分为手动重置和自动重置,区别在于当对象从未触发状态变成触发状态后,会不会自动重置回未触发状态。与其他内核对象相同,事件内核对象也包括引用计数。下面的函数CreateEvent用以创建事件内核对象:
1 2 3 4 5 6 | HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPTSTR lpName ); |
lpEventAttributes:会被忽略,必须为NULL(这里是MSDN的说法,在Windows Via C/C++中,暗示了这个参数与内核对象的安全属性和共享特性有关。不过通常都会传入NULL,因为不太会考虑让一个事件对象变为可继承)
bManualReset:手动重置(TRUE)还是自动重置(FALSE)
bInitialState:初始状态为触发(TRUE)还是非触发(FALSE)
lpName:用于共享内核对象的机制,这里与线程同步无关,不再阐述,传入NULL即可。
返回值:事件对象的句柄
Windows Vista还支持下面这个函数CreateEventEx来创建事件内核对象,详情请参见MSDN:
1 2 3 4 5 6 | HANDLE WINAPI CreateEventEx( __in_opt LPSECURITY_ATTRIBUTES lpEventAttributes, __in_opt LPCTSTR lpName, __in DWORD dwFlags, __in DWORD dwDesiredAccess ); |
我们使用下面的函数来控制事件对象的状态:
1 2 3 4 5 6 7 8 9 10 11 | BOOL SetEvent( HANDLE hEvent ); //将内核对象设置为触发状态 BOOL ResetEvent( HANDLE hEvent ); //将内核对象设置为非触发状态 BOOL PulseEvent( HANDLE hEvent ); //触发内核对象并立即将其重置为未触发状态,会唤醒正在等待的线程 |
如果用图来表示他们的作用就很直观了:
另外OpenEvent通常用于进程同步,但前提是事件内核对象必须有别名(通过给某些内核对象起别名,是一种共享内核对象的方式):
1 2 3 4 5 | HANDLE OpenEvent( DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName ); |
下图描述了事件内核对象的示例用法(同一进程内):
主线程先创建一个事件内核对象,并创建两个线程,这两个线程的执行需要依赖主线程的准备工作,因此调用WaitForSingleObject等待事件对象触发。主线程完成准备工作后,调用SetEvent使对象变成触发状态,这时两个子线程将被唤醒开始执行代码。
在后面的篇章中,将继续介绍其他可以用来线程同步的内核对象。
劳动果实,转载请注明出处:http://www.cnblogs.com/P_Chou/archive/2012/07/03/waitobject-and-event-in-thread-sync.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义