C# 的 ManualResetEvent 类详解
作用
ManualResetEvent
是用于线程同步操作的类,允许一个或多个线程等待特定信号,以协调多个线程的执行顺序。它通过事件通知机制实现,确保线程在收到信号前保持阻塞,直到其他线程显式发出信号。
核心功能
-
阻塞线程:调用
WaitOne()
使线程进入等待状态。 -
发送信号:调用
Set()
将事件设为终止状态,释放所有等待线程。 -
重置信号:调用
Reset()
将事件恢复为非终止状态。
信号状态:
- 终止状态:所有调用
WaitOne(),
线程不会被阻塞,直到调用Reset()。 - 非终止状态:所有调用
WaitOne(),
线程会被阻塞,直到调用set()。
特点
-
手动重置:调用
Set()
后需手动调用Reset()
才能将状态恢复为非终止状态。 -
多线程释放:一旦处于终止状态(Signaled),所有等待线程立即释放,直到手动重置。
-
线程安全:所有方法(
Set
、Reset
、WaitOne
)都是线程安全的。 -
跨进程支持:可通过命名方式在进程间同步(构造函数传名称)。
应用场景
-
初始化同步:主线程等待子线程完成初始化后再继续。
-
资源就绪通知:多个工作线程等待某个共享资源(如数据加载完成)。
-
阶段化任务:分阶段任务中,后续阶段需等待前一阶段所有线程完成。
-
高并发控制:替代锁机制,允许多个线程同时访问资源(需配合
Reset()
)。
基础用法
-
初始化:创建实例,参数指定初始状态(
true
为终止状态)。ManualResetEvent mre = new ManualResetEvent(false); // 初始为非终止
-
阻塞线程:调用
WaitOne()
。mre.WaitOne(); // 阻塞,直到事件变为终止状态
-
发送信号:调用
Set()
。mre.Set(); // 设置为终止状态,释放所有等待线程
-
重置信号:调用
Reset()
。mre.Reset(); // 恢复为非终止状态
代码实例
场景 1:主线程等待子线程完成
用途
主线程需要等待子线程完成某个任务后再继续执行。例如:主线程启动后台任务后需等待其初始化完成,再执行后续操作。
代码逻辑
class Example
{static ManualResetEvent mre = new ManualResetEvent(false);static void Main(){Console.WriteLine("主线程启动工作线程。");Thread worker = new Thread(DoWork);worker.Start();Console.WriteLine("主线程等待信号...");mre.WaitOne(); // 阻塞主线程,直到子线程调用 Set()Console.WriteLine("主线程恢复执行。");}static void DoWork(){Console.WriteLine("工作线程执行任务...");Thread.Sleep(2000); // 模拟耗时操作mre.Set(); // 发送信号,唤醒主线程}
}
执行流程
-
主线程创建
ManualResetEvent
并初始化为非终止状态 (false
)。 -
主线程启动子线程
DoWork
。 -
主线程调用
mre.WaitOne()
进入阻塞状态。 -
子线程执行任务(模拟耗时操作),完成后调用
mre.Set()
,将事件状态设为终止。 -
主线程从
WaitOne()
处解除阻塞,继续执行后续代码。
输出结果
主线程启动工作线程。 主线程等待信号... 工作线程执行任务... (等待 2 秒后) 主线程恢复执行。
场景 2:多个线程等待同一事件
用途
多个工作线程需要等待某个公共条件(如资源初始化完成)满足后,才能同时开始工作。例如:多个线程需等待数据库连接池初始化完成后才可执行查询。
代码逻辑
using System;
using System.Threading;class Example
{static ManualResetEvent mre = new ManualResetEvent(false);static void Main(){// 启动 3 个工作线程for (int i = 0; i < 3; i++){new Thread(Worker).Start(i);}// 启动初始化线程new Thread(Initialize).Start();}static void Initialize(){Console.WriteLine("初始化开始...");Thread.Sleep(3000);Console.WriteLine("初始化完成!");mre.Set(); // 通知所有等待线程}static void Worker(object id){Console.WriteLine($"线程 {id} 等待初始化...");mre.WaitOne(); // 阻塞,直到初始化线程调用 Set()Console.WriteLine($"线程 {id} 开始工作。");}
}
执行流程
-
主线程启动 3 个工作线程和一个初始化线程。
-
每个工作线程调用
mre.WaitOne()
进入阻塞状态,等待初始化完成。 -
初始化线程执行耗时操作(如加载配置),完成后调用
mre.Set()
。 -
所有等待的工作线程同时被唤醒,开始执行后续任务。
输出结果
线程 0 等待初始化... 线程 1 等待初始化... 线程 2 等待初始化... 初始化开始... (等待 3 秒后) 初始化完成! 线程 0 开始工作。 线程 1 开始工作。 线程 2 开始工作。
场景 3:重复使用 ManualResetEvent
用途
需要多次复用同一个 ManualResetEvent
实例,分阶段同步多个任务。例如:分批次处理数据,每批任务完成后触发下一批任务。
代码逻辑
using System;
using System.Threading;class Example
{static ManualResetEvent mre = new ManualResetEvent(false);static void Main(){// 首次使用new Thread(() => Task("任务1")).Start();mre.WaitOne();mre.Reset(); // 重置为非终止状态// 第二次使用new Thread(() => Task("任务2")).Start();mre.WaitOne();}static void Task(string name){Console.WriteLine($"{name} 进行中...");Thread.Sleep(1000);mre.Set();}
}
执行流程
-
主线程启动第一个子线程执行
任务1
。 -
主线程调用
mre.WaitOne()
阻塞,等待任务1
完成。 -
子线程
任务1
完成后调用mre.Set()
,主线程恢复执行。 -
主线程调用
mre.Reset()
将事件重置为非终止状态。 -
主线程启动第二个子线程执行
任务2
,再次调用mre.WaitOne()
阻塞。 -
子线程
任务2
完成后调用mre.Set()
,主线程恢复执行。
输出结果
任务1 进行中... (等待 1 秒后) 任务2 进行中... (等待 1 秒后)
三个场景关键区别总结
场景 | 核心目的 | ManualResetEvent 操作要点 |
---|---|---|
主线程等待子线程 | 单向等待子线程完成 | 子线程完成时调用 Set() |
多线程等待同一事件 | 广播式唤醒所有等待线程 | Set() 后无需立即 Reset() |
重复使用事件对象 | 分阶段同步任务 | 每次使用后需调用 Reset() 重置状态 |
通过这三个场景,可以灵活掌握 ManualResetEvent
在不同线程同步需求中的使用技巧。