在C#多线程开发中,ManualResetEvent是一个常用的同步原语,它基于操作系统内核事件对象实现,允许一个或多个线程阻塞等待,直到另一个线程发出信号。与自动复位事件不同,ManualResetEvent在被置为有信号状态后,会一直保持该状态,直到显式调用Reset方法,这意味着它可以同时释放所有正在等待的线程,而不是每次只放一个。

ManualResetEvent的基本工作机制
ManualResetEvent继承自EventWaitHandle,构造时接受一个布尔参数表示初始状态。如果传入true,事件创建后即为有信号状态,调用WaitOne的线程不会阻塞;如果为false,则线程必须在WaitOne处等待,直到其他线程调用Set。底层而言,它对应一个内核中的事件对象,WaitOne会使调用线程进入等待队列,而Set则将该事件设为激发态,唤醒所有等待者。
下面的代码展示了最基本的用法:主线程创建ManualResetEvent并启动工作线程,工作线程在事件上等待,主线程在准备完毕后调用Set放行。
using System;
using System.Threading;
class Program
{
static ManualResetEvent mre = new ManualResetEvent(false);
static void Worker()
{
Console.WriteLine("工作线程等待信号");
mre.WaitOne();
Console.WriteLine("工作线程收到信号,继续执行");
}
static void Main()
{
Thread t = new Thread(Worker);
t.Start();
Thread.Sleep(1000);
Console.WriteLine("主线程发出信号");
mre.Set();
t.Join();
}
}
从上述例子可以看到,ManualResetEvent的核心价值在于“手动管理信号生命周期”。因为Set之后状态不自动清除,如果忘记调用Reset,后续再次WaitOne的线程将直接通过,不会阻塞。这在需要重复使用的屏障场景中既是优点也是隐患,开发者必须清楚当前事件处于何种状态。
与AutoResetEvent的关键差异及选型
很多开发者混淆ManualResetEvent和AutoResetEvent,导致同步逻辑错误。AutoResetEvent在释放一个等待线程后会自动恢复无信号状态,相当于每次Set只服务一个线程;而ManualResetEvent需要手动Reset,一次Set可服务多个线程。如果场景是“多个线程等待同一个开始指令”,显然ManualResetEvent更合适,因为不需要反复Set。
考虑如下对比:假设有五个工作线程都需要同时开始处理数据,使用AutoResetEvent时,必须调用五次Set或者让线程间链式唤醒,逻辑复杂且容易遗漏;而ManualResetEvent只需一次Set,所有线程一并启动。反过来,如果是“生产者每次只通知一个消费者取任务”,AutoResetEvent更自然,避免手动Reset的麻烦。
using System;
using System.Threading;
class CompareDemo
{
static ManualResetEvent mre = new ManualResetEvent(false);
static AutoResetEvent are = new AutoResetEvent(false);
static void RunMre()
{
for (int i = 0; i < 3; i++)
{
new Thread(() =>
{
mre.WaitOne();
Console.WriteLine("MRE线程执行: " + Thread.CurrentThread.ManagedThreadId);
}).Start();
}
Thread.Sleep(500);
mre.Set(); // 一次性释放全部
}
static void RunAre()
{
for (int i = 0; i < 3; i++)
{
new Thread(() =>
{
are.WaitOne();
Console.WriteLine("ARE线程执行: " + Thread.CurrentThread.ManagedThreadId);
}).Start();
}
Thread.Sleep(500);
are.Set(); // 只释放一个,需多次Set
are.Set();
are.Set();
}
static void Main()
{
RunMre();
Thread.Sleep(1000);
RunAre();
}
}
通过这段代码可以直观看到,ManualResetEvent在广播场景中代码更简洁,但务必在下一轮等待前调用Reset,否则会失去同步作用。AutoResetEvent则适合点对点通知,不容易因遗忘复位而产生逻辑漏洞。选型时应先明确通知是一对多还是一对一。
常见使用陷阱与健壮写法
实际项目中,ManualResetEvent最容易引发两个问题:一是忘记Reset导致后续等待失效,二是异常路径下未释放信号造成死锁。例如工作线程在WaitOne之后执行业务逻辑时抛出异常,若主线程依赖Reset来控制循环批次,就可能永远无法进入下一轮阻塞。
为了避免这类问题,推荐将WaitOne与业务处理放在try-finally外部管理状态,或者使用带超时的WaitOne并及时检查返回值。下方示例演示了带超时和状态检查的写法,既防止无限等待,也方便记录超时原因。
using System;
using System.Threading;
class SafeUsage
{
static ManualResetEvent mre = new ManualResetEvent(false);
static void Worker(int id)
{
if (mre.WaitOne(2000))
{
try
{
Console.WriteLine("线程" + id + "开始工作");
// 模拟业务
Thread.Sleep(200);
}
catch (Exception ex)
{
Console.WriteLine("线程" + id + "异常: " + ex.Message);
}
}
else
{
Console.WriteLine("线程" + id + "等待超时");
}
}
static void Main()
{
for (int i = 1; i <= 3; i++)
{
int copy = i;
new Thread(() => Worker(copy)).Start();
}
Thread.Sleep(1000);
mre.Set();
// 下一轮使用前复位
mre.Reset();
Console.WriteLine("已复位,可再次等待");
}
}
此外,在长时间运行的服务中,建议把ManualResetEvent实例作为类成员并配合CancellationToken使用,当服务停止时调用Set让线程退出等待,同时在finally中Dispose释放内核句柄。这样可以避免资源泄漏,也符合.NET中规范的非托管资源管理方式。掌握这些细节,才能在复杂并发系统中稳定运用ManualResetEvent。
ManualResetEventC#线程同步WaitOne修改时间:2026-08-13 18:15:37