C# 的 ManualResetEvent(线程同步操作) 类详解

发布于:2025-03-14 ⋅ 阅读:(15) ⋅ 点赞:(0)

C# 的 ManualResetEvent 类详解

作用

ManualResetEvent 是用于线程同步操作的类,允许一个或多个线程等待特定信号,以协调多个线程的执行顺序。它通过事件通知机制实现,确保线程在收到信号前保持阻塞,直到其他线程显式发出信号。

核心功能
  • 阻塞线程:调用 WaitOne() 使线程进入等待状态。

  • 发送信号:调用 Set() 将事件设为终止状态,释放所有等待线程。

  • 重置信号:调用 Reset() 将事件恢复为非终止状态。


信号状态:
  • 终止状态:所有调用 WaitOne(),线程不会被阻塞,直到调用Reset()。
  • 非终止状态:所有调用 WaitOne(),线程会被阻塞,直到调用set()。

特点
  1. 手动重置:调用 Set() 后需手动调用 Reset() 才能将状态恢复为非终止状态。

  2. 多线程释放:一旦处于终止状态(Signaled),所有等待线程立即释放,直到手动重置。

  3. 线程安全:所有方法(SetResetWaitOne)都是线程安全的。

  4. 跨进程支持:可通过命名方式在进程间同步(构造函数传名称)。


应用场景
  1. 初始化同步:主线程等待子线程完成初始化后再继续。

  2. 资源就绪通知:多个工作线程等待某个共享资源(如数据加载完成)。

  3. 阶段化任务:分阶段任务中,后续阶段需等待前一阶段所有线程完成。

  4. 高并发控制:替代锁机制,允许多个线程同时访问资源(需配合 Reset())。


基础用法
  1. 初始化:创建实例,参数指定初始状态(true 为终止状态)。

    ManualResetEvent mre = new ManualResetEvent(false); // 初始为非终止
  2. 阻塞线程:调用 WaitOne()

    mre.WaitOne(); // 阻塞,直到事件变为终止状态
  3. 发送信号:调用 Set()

    mre.Set(); // 设置为终止状态,释放所有等待线程
  4. 重置信号:调用 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(); // 发送信号,唤醒主线程
    }
}
执行流程
  1. 主线程创建 ManualResetEvent 并初始化为非终止状态 (false)。

  2. 主线程启动子线程 DoWork

  3. 主线程调用 mre.WaitOne() 进入阻塞状态。

  4. 子线程执行任务(模拟耗时操作),完成后调用 mre.Set(),将事件状态设为终止。

  5. 主线程从 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} 开始工作。");
    }
}
执行流程
  1. 主线程启动 3 个工作线程和一个初始化线程。

  2. 每个工作线程调用 mre.WaitOne() 进入阻塞状态,等待初始化完成。

  3. 初始化线程执行耗时操作(如加载配置),完成后调用 mre.Set()

  4. 所有等待的工作线程同时被唤醒,开始执行后续任务。

输出结果
线程 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. 主线程启动第一个子线程执行 任务1

  2. 主线程调用 mre.WaitOne() 阻塞,等待 任务1 完成。

  3. 子线程 任务1 完成后调用 mre.Set(),主线程恢复执行。

  4. 主线程调用 mre.Reset() 将事件重置为非终止状态。

  5. 主线程启动第二个子线程执行 任务2,再次调用 mre.WaitOne() 阻塞。

  6. 子线程 任务2 完成后调用 mre.Set(),主线程恢复执行。

输出结果
任务1 进行中...
(等待 1 秒后)
任务2 进行中...
(等待 1 秒后)

三个场景关键区别总结

场景 核心目的 ManualResetEvent 操作要点
主线程等待子线程 单向等待子线程完成 子线程完成时调用 Set()
多线程等待同一事件 广播式唤醒所有等待线程 Set() 后无需立即 Reset()
重复使用事件对象 分阶段同步任务 每次使用后需调用 Reset() 重置状态

通过这三个场景,可以灵活掌握 ManualResetEvent 在不同线程同步需求中的使用技巧。