C# ManualResetEvent 类 使用详解

总目录


前言

ManualResetEvent 是 C# 中用于线程同步的核心类之一,位于 System.Threading 命名空间下。它的核心功能是通过信号机制控制线程的执行顺序,允许一个或多个线程等待某个信号后再继续运行。与 AutoResetEvent 不同,ManualResetEvent 在被触发后会保持信号状态,直到显式地调用 Reset() 方法将其重置为非信号状态。这种特性使得它适用于需要广播多个线程的场景。


一、核心概念

  • 作用:通过信号机制控制线程的执行,允许一个或多个线程等待某个事件完成。
  • 信号状态
    • 有信号(Signaled) :所有调用 WaitOne() 的线程不会被阻塞。
    • 无信号(Non-signaled) :所有调用 WaitOne() 的线程会被阻塞,直到调用 Set()
  • 手动重置
    • 调用 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 复制代码
等待中,请发出信号允许我运行
等待中,请发出信号允许我运行
等待中,请发出信号允许我运行
等待中,请发出信号允许我运行
等待中,请发出信号允许我运行
等待中,请发出信号允许我运行
线程结束
  • AutoResetEventWaitOne() 方法等待信号完毕后,会自动重置为无信号状态,相当于高速收费站自动闸门,一辆车过去后,机器自动关闸。

  • 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

相关推荐
m0_748233889 分钟前
JAVA程序员面试总结
java·开发语言·面试
轩源源27 分钟前
数据结构——红黑树的实现
开发语言·数据结构·c++·算法·红黑树·单旋+变色·双旋+变色
Orange30151127 分钟前
ES6~ES11新特性全解析
java·前端·javascript·es6
Hello.Reader39 分钟前
Rust 命令行参数解析:以 minigrep 为例
开发语言·chrome·rust
Lanwarf-前端开发1 小时前
gis风场加载
开发语言·前端·javascript
程序员林北北1 小时前
【Golang学习之旅】gRPC 与 REST API 的对比及应用
java·开发语言·后端·学习·云原生·golang
钢铁男儿1 小时前
Python 字典(一个简单的字典)
开发语言·python
游王子2 小时前
Python Pandas(7):Pandas 数据清洗
开发语言·python·pandas
一川晚照人闲立2 小时前
JEECGBOOT前端VUE3版本浏览器兼容支持chrome>=76版本方法
java·前端·vue.js·chrome·anti-design-vue·jeecgboot·jeecg
0wioiw02 小时前
Python基础(SQLAlchemy)
java·开发语言·数据库