总目录
前言
ManualResetEvent
是 C# 中用于线程同步的核心类之一,位于 System.Threading 命名空间下。它的核心功能是通过信号机制控制线程的执行顺序,允许一个或多个线程等待某个信号后再继续运行。与 AutoResetEvent 不同,ManualResetEvent 在被触发后会保持信号状态,直到显式地调用 Reset() 方法将其重置为非信号状态。这种特性使得它适用于需要广播多个线程的场景。
一、核心概念
- 作用:通过信号机制控制线程的执行,允许一个或多个线程等待某个事件完成。
- 信号状态 :
- 有信号(Signaled) :所有调用
WaitOne()
的线程不会被阻塞。 - 无信号(Non-signaled) :所有调用
WaitOne()
的线程会被阻塞,直到调用Set()
。
- 有信号(Signaled) :所有调用
- 手动重置 :
- 调用
Set()
后,事件保持有信号状态,需显式调用Reset()
才能恢复无信号状态。 - 意味着可以释放多个等待的线程。
- 调用
本文中所描述的 有信号状态、终止状态、触发状态 意义相同,都是同一种状态的不同名称
二、基本用法
1. 构造函数
csharp
var manualEvent = new ManualResetEvent(initialState: false); // 初始无信号
initialState
:初始化是否为有信号状态(true 表示有信号/或称 已触发,则线程一开始是无需等待信号的)。
2. 关键方法
方法 | 作用 |
---|---|
Set() |
将事件设为有信号状态,释放所有等待线程。 |
Reset() |
将事件设为无信号状态,后续的 WaitOne() 会阻塞。 |
WaitOne() |
阻塞当前线程,直到事件变为有信号状态。可以指定超时时间 |
Dispose() |
释放资源(继承自 WaitHandle )。 |
三、 示例
示例 1:单线程等待事件
csharp
using System.Threading;
class Program
{
static ManualResetEvent manualEvent = new ManualResetEvent(false);
static void Main()
{
Thread worker = new Thread(DoWork);
worker.Start();
// 主线程触发信号
Thread.Sleep(2000);
Console.WriteLine("主线程发送信号");
manualEvent.Set(); // 释放工作线程
}
static void DoWork()
{
Console.WriteLine("工作线程等待信号...");
manualEvent.WaitOne(); // 阻塞直到信号触发
Console.WriteLine("工作线程继续执行");
}
}
输出:
工作线程等待信号...
主线程发送信号
工作线程继续执行
示例 2:广播多个线程
csharp
static ManualResetEvent manualEvent = new ManualResetEvent(false);
static void Main()
{
// 启动3个等待线程
for (int i = 0; i < 3; i++)
{
new Thread(() =>
{
manualEvent.WaitOne();
Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 被唤醒");
}).Start();
}
// 主线程触发广播
Thread.Sleep(2000);
manualEvent.Set(); // 所有等待线程同时释放
manualEvent.Reset(); // 重置为无信号,后续新线程需要重新等待
}
输出:
线程 4 被唤醒
线程 5 被唤醒
线程 6 被唤醒
四、与 AutoResetEvent
的区别
1. 区别
特性 | ManualResetEvent |
AutoResetEvent |
---|---|---|
重置方式 | 需显式调用 Reset() |
调用 Set() 后自动重置 |
唤醒线程数量 | 唤醒所有等待线程 | 每次 Set() 仅唤醒一个线程 |
适用场景 | 广播通知(如初始化完成、批量任务开始) | 严格交替执行(如生产者-消费者模型) |
2. 示例
- AutoResetEvent 示例
csharp
class Program
{
// 线程通知
private static AutoResetEvent resetEvent = new AutoResetEvent(false);
static void Main(string[] args)
{
// 创建线程
Thread worker = new Thread(DoWork);
worker.Start();
// 用于不断向另一个线程发送信号
while (true)
{
Console.ReadKey();
resetEvent.Set(); // 按下任意键,将事件设为有信号状态,释放等待线程。
}
}
public static void DoWork()
{
Console.WriteLine("① 等待中,请发出信号允许我运行");
resetEvent.WaitOne();
Console.WriteLine("② 等待中,请发出信号允许我运行");
resetEvent.WaitOne();
Console.WriteLine("③ 等待中,请发出信号允许我运行");
// ...
Console.WriteLine("线程结束");
}
}
输出:按下任意键,按一下输出一下内容
csharp
① 等待中,请发出信号允许我运行
② 等待中,请发出信号允许我运行
③ 等待中,请发出信号允许我运行
线程结束
- ManualResetEvent 示例
csharp
class Program
{
private static ManualResetEvent resetEvent = new ManualResetEvent(false);
static void Main(string[] args)
{
// 创建线程
Thread worker = new Thread(DoWork);
worker.Start();
// 用于不断向另一个线程发送信号
while (true)
{
Console.ReadKey();
resetEvent.Set(); // 按下任意键,将事件设为有信号状态,释放【所有】等待线程。
}
}
public static void DoWork()
{
Console.WriteLine("等待中,请发出信号允许我运行");
resetEvent.WaitOne();
// 后面的都无效,线程会直接跳过而无需等待
Console.WriteLine("等待中,请发出信号允许我运行");
resetEvent.WaitOne();
Console.WriteLine("等待中,请发出信号允许我运行");
resetEvent.WaitOne();
Console.WriteLine("等待中,请发出信号允许我运行");
resetEvent.WaitOne();
Console.WriteLine("等待中,请发出信号允许我运行");
resetEvent.WaitOne();
Console.WriteLine("等待中,请发出信号允许我运行");
resetEvent.WaitOne();
Console.WriteLine("线程结束");
}
}
输出:按下任意键,直接输出所有内容
csharp
等待中,请发出信号允许我运行
等待中,请发出信号允许我运行
等待中,请发出信号允许我运行
等待中,请发出信号允许我运行
等待中,请发出信号允许我运行
等待中,请发出信号允许我运行
线程结束
-
AutoResetEvent
在WaitOne
() 方法等待信号完毕后,会自动重置为无信号状态,相当于高速收费站自动闸门,一辆车过去后,机器自动关闸。 -
ManualResetEvent
相当于人工闸门,打开后需要人工关闭闸门,不然的话闸门会一直处于打开状态。 -
ManualResetEvent 主要用于更加灵活的线程信号传递场景。
五、高级用法
1. 超时等待
csharp
bool signaled = manualEvent.WaitOne(TimeSpan.FromSeconds(3));
if (!signaled)
{
Console.WriteLine("等待超时");
}
六、注意事项
1. 资源释放:
- 使用
Dispose()
或using
块释放资源,避免句柄泄漏。
csharp
using (var manualEvent = new ManualResetEvent(false))
{
// 使用 manualEvent
}
2. 避免死锁:
- 确保
Set()
和Reset()
的调用逻辑合理,避免线程永久阻塞。 - 示例:忘记调用
Set()
或Reset()
。
3. 线程安全:
- 多线程环境下,确保对
Set()
和Reset()
的调用是线程安全的。
七、替代方案
ManualResetEventSlim
:轻量级版本,性能更高(适合短期等待)。Semaphore
/SemaphoreSlim
:控制并发线程数量。TaskCompletionSource
:基于任务的异步模式(TAP)。
八、常见问题
1. 问题:忘记调用 Reset()
- 现象 :
Set()
后事件保持有信号状态,后续所有WaitOne()
直接通过。 - 解决 :在需要重新阻塞线程前调用
Reset()
。
2. 问题:多次调用 Set()
- 现象 :无影响,事件已处于有信号状态时,
Set()
不会改变状态。
结语
回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
ManualResetEvent的使用
C# ManualResetEvent 类的用法
C#学习(二十八)------ManualResetEvent的理解和使用
手动线程通知 ManualResetEvent