总目录
前言
在C#中,AutoResetEvent是一个用于线程同步的关键类,它位于System.Threading命名空间下。它的核心功能是通过信号机制控制线程的执行顺序,允许一个或多个线程等待某个信号后再继续运行。常用于需要线程间通信的场景。
一、核心概念
- 作用:
AutoResetEvent是一种同步原语,用于在多线程环境中实现线程间的等待和通知- 通知等待的线程某个事件已发生(如资源就绪、操作完成)。
- 信号状态:
- 有信号(Signaled):线程调用
WaitOne()不会被阻塞。 - 无信号(Non-signaled):线程调用
WaitOne()会被阻塞,直到信号触发。
- 有信号(Signaled):线程调用
- 自动重置:
- 当一个等待线程被释放后,AutoResetEvent 会自动将状态重置为非信号状态(即非触发状态)。这意味着每次仅允许一个等待线程继续执行。
本文中所描述的 有信号状态、终止状态、触发状态 意义相同,都是同一种状态的不同名称
AutoResetEvent 理解成 一扇门 true 开门信号,false 关门信号,当有开门信号的时候 就开门,反之,则关门
当使用 AutoResetEvent 的 WaitOne的时候,就是等待 开门信号,开门就可通行,通行后 自动关闭
当使用 AutoResetEvent 的 Set 的时候,就是 发送 开门信息,给关闭的门 发送开门信息,即可开门
当使用 AutoResetEvent 的 Reset 的时候,就是 发送 关闭信息,给开着的门 发送关门信息,即可关门
二、基本用法
1. 构造函数
varautoEvent=newAutoResetEvent(initialState:false);// 初始无信号/或称 未触发initialState:初始化是否为有信号状态(true表示有信号/或称 已触发,则线程一开始是无需等待信号的)。
2. 关键方法
WaitOne():阻塞当前线程,直到事件变为有信号。可以指定超时时间。autoEvent.WaitOne();// 阻塞直到信号触发- 如果 AutoResetEvent 已经处于有信号状态,那么线程调用 WaitOne() 不会再起阻塞线程的作用。除非调用Reset() 。
Set():将事件设置为有信号状态(释放一个等待线程)。autoEvent.Set();// 发送信号,唤醒一个等待线程- 当调用 Set() 时,有线程在等待,会释放一个线程并自动重置为无信号状态。
- 若调用 Set() 时,没有线程等待,事件保持有信号状态,直到后续的 WaitOne() 调用自动消费信号并重置。
Reset():将事件设置为无信号状态autoEvent.Reset();- 手动 Reset() 的用途:
- 强制将事件设置为无信号状态,无论当前是否有线程在等待。
- 确保下一轮的 WaitOne() 必须等待对方的信号,避免因事件残留信号导致逻辑错误。
- 手动 Reset() 的用途:
注意:
- 有信号和无信号状态指的是AutoResetEvent的状态,而不是线程的状态
- 如果多次调用 Set() 的时间间隔过短,如果第一次 Set() 还没有结束(信号发送需要处理时间),那么第二次 Set() 可能无效(不起作用)。
3. 主要场景
| 场景 | 描述 |
|---|---|
| 生产者-消费者队列 | 生产线程入队一个任务后Set(),消费者线程WaitOne()等待,每次只唤醒一个消费者处理任务。 |
| 等待异步操作完成 | 主线程启动一个异步操作,用AutoResetEvent等待操作完成,操作完成后Set()通知。 |
| 控制线程池中任务逐个执行 | 多个工作线程等待信号,主控线程逐步释放每个任务。 |
实现简单的锁(但通常用lock更好) | 用于一次性许可的互斥,但性能较差。 |
| 模拟“门禁” | 一次只允许一个线程进入某个阶段,例如游戏回合制。 |
三、 示例
示例 1:线程等待主线程信号
usingSystem.Threading;classProgram{staticAutoResetEventautoEvent=newAutoResetEvent(false);staticvoidMain(){Threadworker=newThread(DoWork);worker.Start();// 主线程触发信号Thread.Sleep(2000);Console.WriteLine("主线程发送信号");autoEvent.Set();// 唤醒工作线程}staticvoidDoWork(){Console.WriteLine("工作线程等待信号...");autoEvent.WaitOne();// 阻塞直到信号到来Console.WriteLine("工作线程继续执行");}}输出
工作线程等待信号... 主线程发送信号 工作线程继续执行示例 2:主线程等待工作线程信号
usingSystem;usingSystem.Threading;classProgram{staticAutoResetEvent_autoResetEvent=newAutoResetEvent(false);staticvoidMain(string[]args){// 启动一个新线程Threadt=newThread(Work);t.Start();Console.WriteLine("主线程等待...");// 主线程等待_autoResetEvent.WaitOne();Console.WriteLine("主线程继续执行");}staticvoidWork(){Console.WriteLine("工作线程正在工作...");// 模拟一些工作Thread.Sleep(1000);Console.WriteLine("工作完成,通知主线程");// 发送信号,允许等待的线程继续_autoResetEvent.Set();}}在这个例子中,主线程调用 _autoResetEvent.WaitOne(); 等待,而工作线程在完成任务后调用 _autoResetEvent.Set(); 通知主线程继续执行。
输出
主线程等待...工作线程正在工作...工作完成,通知主线程 主线程继续执行示例3:线程执行控制
classProgram{// 控制第一个线程// 第一个线程开始时,AutoResetEvent 处于终止状态,无需等待信号privatestaticAutoResetEventoneResetEvent=newAutoResetEvent(true);// 控制第二个线程// 第二个线程开始时,AutoResetEvent 处于非终止状态,需要等待信号privatestaticAutoResetEventtwoResetEvent=newAutoResetEvent(false);staticvoidMain(string[]args){newThread(DoOne).Start();newThread(DoTwo).Start();Console.ReadKey();}publicstaticvoidDoOne(){while(true){Console.WriteLine("\n① 按下任意键,我就让DoTwo运行");Console.ReadKey();twoResetEvent.Set();oneResetEvent.Reset();// 等待 DoTwo() 给我信号oneResetEvent.WaitOne();Console.ForegroundColor=ConsoleColor.Green;Console.WriteLine("\n DoOne() 执行");Console.ForegroundColor=ConsoleColor.White;}}publicstaticvoidDoTwo(){while(true){Thread.Sleep(TimeSpan.FromSeconds(1));// 等待 DoOne() 给我信号twoResetEvent.WaitOne();Console.ForegroundColor=ConsoleColor.Yellow;Console.WriteLine("\n DoTwo() 执行");Console.ForegroundColor=ConsoleColor.White;Console.WriteLine("\n② 按下任意键,我就让DoOne运行");Console.ReadKey();oneResetEvent.Set();twoResetEvent.Reset();}}}代码分析
DoOne 线程流程
- 调用 twoResetEvent.Set():通知 DoTwo 可以运行。
- 手动调用 oneResetEvent.Reset():强制将 oneResetEvent 设为非终止状态。
- 调用 oneResetEvent.WaitOne():阻塞等待 DoTwo 的 Set()。
DoTwo 线程流程
- 调用 twoResetEvent.WaitOne():等待 DoOne 的信号。
- 执行完成后,调用 oneResetEvent.Set():通知 DoOne 继续。
- 手动调用 twoResetEvent.Reset():强制将 twoResetEvent 设为非终止状态。
关键点
- 手动 Reset() 的目的是立即重置事件状态,确保下一轮循环必须严格等待对方的信号。
- 若省略 Reset(),事件可能残留终止状态,导致 WaitOne() 直接通过,破坏交替逻辑。
- 必须保留 Reset():
- oneResetEvent.Reset() 确保 DoOne 必须等待 DoTwo 的信号。
- twoResetEvent.Reset() 确保 DoTwo 必须等待 DoOne 的信号。
- 若省略,残留信号会导致线程间同步失效,破坏交替执行的预期逻辑。
省略 Reset() 场景:省略 oneResetEvent.Reset()
- 假设 DoOne 未调用 oneResetEvent.Reset():
- DoOne 调用 twoResetEvent.Set(),通知 DoTwo 运行。
- DoOne 调用 oneResetEvent.WaitOne()。
- 若 oneResetEvent 仍为终止状态(例如,DoTwo 在上一轮已调用 Set()),WaitOne() 会直接通过,无需等待。
- 导致 DoOne 和 DoTwo 同时执行,破坏交替逻辑。
- 假设 DoOne 未调用 oneResetEvent.Reset():
四、高级用法
1. 超时等待
boolsignaled=autoEvent.WaitOne(TimeSpan.FromSeconds(5));// 最多等待5秒if(!signaled){Console.WriteLine("等待超时");}2. 多线程竞争
// 多个线程等待同一个事件for(inti=0;i<3;i++){newThread(()=>{autoEvent.WaitOne();Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}被唤醒");}).Start();}// 需要调用 Set() 三次才能唤醒所有线程autoEvent.Set();// 唤醒第一个线程autoEvent.Set();// 唤醒第二个线程autoEvent.Set();// 唤醒第三个线程5、 注意事项
- 资源释放:用完需调用
Dispose()或使用using块(AutoResetEvent继承自WaitHandle)。 - 竞态条件:确保
Set()和WaitOne()的调用顺序合理,避免死锁。 - 自动重置导致“丢失信号”:如果 Set() 发生时没有线程在等待,则该信号会丢失,后续 WaitOne() 会继续阻塞。确保至少有一个线程在等待,或者使用 WaitOne(0) 检查状态。
- 避免死锁:若持有 AutoResetEvent 的线程未 Set(),等待线程将永久阻塞。建议使用超时重载 WaitOne(int millisecondsTimeout)。
- 不适用于“广播”:要唤醒所有线程,应使用 ManualResetEvent。
- 与 ManualResetEvent 的区别:
AutoResetEvent自动重置信号,适合一次唤醒一个线程。ManualResetEvent需手动调用Reset(),适合广播多个线程。
六、总结
AutoResetEvent的本质:一个自动重置的信号量,每次 Set() 只放行一个等待线程。适用场景:生产者-消费者模式、任务队列、异步操作等待、需要“一对一”通知的场景。
与 ManualResetEvent 的区别:AutoResetEvent 自动关门,ManualResetEvent 手动关门,所以前者用于唤醒单个线程,后者用于唤醒所有线程。
与 SemaphoreSlim 的区别:SemaphoreSlim 可控制并发计数(允许多个),且是托管实现,性能更好,但仅限进程内;AutoResetEvent 只允许一个线程通过,且可跨进程。
选择建议:
进程内一对一通知 → 优先考虑 TaskCompletionSource 或 Monitor.Wait/Pulse(性能更好),若需超时或跨进程则用 AutoResetEvent。
进程内多并发控制 → SemaphoreSlim。
广播通知 → ManualResetEvent。
掌握 AutoResetEvent,能让你更好地控制线程协作,但在现代 C# 中,更推荐使用 Task 和 async/await 来简化异步流程,仅在需要底层同步时使用这些经典基元。
结语
回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
自动线程通知 AutoResetEvent