C#多线程控制:使用CountdownEvent精准等待任务完成

一颗松果
一颗松果
发布时间:2026-10-16 | 4人浏览过
原创

在C#多线程场景中,协调多个后台任务并在所有任务结束后执行后续逻辑是常见需求。传统的轮询计数方式存在线程安全风险和阻塞问题。本文将详细介绍如何使用 CountdownEvent 类实现高效的线程完成数监控,通过其信号通知机制,确保主线程在所有子任务完成后安全继续执行。

传统计数法的痛点

假设我们需要向服务器发送5个请求,并允许在网络波动时自动重试。如果使用简单的全局计数器记录完成任务的数量,往往会导致代码混乱且难以处理动态变化的线程数(如重试增加新线程)。

以下是一个存在缺陷的示例:

    class Program
    {
        private static int count = 0;
        static void Main(string[] args)
        {
            for (int i = 0; i < 5; i++)
                new Thread(HttpRequest).Start();            // 创建线程

            // 用于不断向另一个线程发送信号
            while (count < 5)
            {
                Thread.Sleep(100);
            }
            Console.WriteLine("任务执行完毕");
        }


        // 模拟网络请求
        public static void HttpRequest()
        {
            Console.WriteLine("开始一个任务");
            // 随机生成一个数,如果为偶数,则模拟请求失败
            bool isSuccess = (new Random().Next(0, 10)) % 2 == 0;

            // ... ...模拟请求 HTTP
            Thread.Sleep(TimeSpan.FromSeconds(2));

            // 请求失败则重试
            if (!isSuccess)
            {
                Console.WriteLine($"请求失败,count={count}");
                new Thread(() =>
                {
                    HttpRequest();
                }).Start();
                return;
            }
            // 完成一次任务,+1
            Interlocked.Add(ref count,1);
            Console.WriteLine($"完成任务,count={count}");
        }
    }

上述代码虽然使用了 Interlocked.Add 保证计数的原子性,但存在明显缺陷: 1. 忙等待:while 循环配合 Thread.Sleep 浪费CPU资源。 2. 逻辑脆弱:当发生重试时,新线程加入后,原定的5次完成条件可能无法正确触发或导致逻辑错位。 3. 扩展性差:难以应对动态增减任务场景。

CountdownEvent 的原理

System.Threading.CountdownEvent 是一个同步基元,它维护一个内部计数器。每个线程完成任务时发出“信号”(Signal),计数器减1。当计数器归零时,等待该事件的线程将被唤醒。

这完美契合了“等待N个任务完成”的场景,且支持动态调整计数(例如任务失败需要重试时增加计数)。

核心方法与构造函数

CountdownEvent 的关键 API 如下:

  • 构造函数:CountdownEvent(Int32):初始化指定初始值的计数器。
  • Signal():发出信号,使 CurrentCount 减1。这是每个工作线程完成任务后的标准动作。
  • Wait():阻塞当前线程,直到 CurrentCount 变为0。可传入超时时间或 CancellationToken 以避免无限等待。
  • AddCount():将计数器加1。常用于任务失败需要重新分配给新线程或重试的场景,防止计数器提前归零导致主线程过早退出。
  • Reset():将计数器重置为初始值,以便复用同一个 CountdownEvent 实例进行下一轮任务协调。

标准实战示例

下面展示如何使用 CountdownEvent 优雅地解决上述多任务等待问题。设定5个任务,每个任务独立执行,主线程等待所有任务完成后打印结果。

    class Program
    {
        // 手头上有 5 件事
        private static CountdownEvent countd = new CountdownEvent(5);
        static void Main(string[] args)
        {
            Console.WriteLine("开始交待任务");
            // 同时叫 5 个人,去做 5 件事
            for (int i = 0; i < 5; i++)
            {
                Thread thread = new Thread(DoOne);
                thread.Name = $"{i}";
                thread.Start();
            }


            // 等他们都完成事情
            countd.Wait();

            Console.WriteLine("任务完成,线程退出");
            Console.ReadKey();
        }

        public static void DoOne()
        {
            int n = new Random().Next(0, 10);
            // 模拟要 n 秒才能完成
            Thread.Sleep(TimeSpan.FromSeconds(n));
            // 完成了,减去一件事
            countd.Signal();
            Console.WriteLine($"    {Thread.CurrentThread.Name}完成一件事了");
        }
    }

关键要点解析

  1. 初始化计数:在 Main 中创建 CountdownEvent(5),表明期望有5个信号到来。
  2. 工作线程调用 Signal:每个子线程 DoOne 在业务逻辑执行完毕后,必须调用 countd.Signal()。这会原子性地减少计数器并检查是否归零。
  3. 主线程调用 Wait:countd.Wait() 会挂起主线程,直到所有5个任务的 Signal 都到达。这比轮询更高效且无状态污染。

进阶处理:异常与重试

在实际生产环境中,任务可能会失败。如果某个任务失败且不重试,直接不调用 Signal 会导致主线程永久阻塞(Deadlock)。如果重试,则需要动态增加计数器。

例如,若任务失败需要启动一个新线程重试:

  1. 不要立即 Signal:在主任务失败时,先调用 countd.AddCount() 将计数器+1(抵消即将消失的一个成功信号),然后启动新线程。
  2. 新线程正常 Signal:新线程完成后正常调用 Signal。
  3. 或者超时保护:使用 Wait(TimeSpan timeout) 设置超时时间,防止因未调用的 Signal 导致的死锁。

总结来说,CountdownEvent 提供了比手动轮询更安全、更清晰的线程协调方案。它让代码意图明确:“等待N个事件”,并内置了线程安全的计数管理。

以上就是C#多线程中使用CountdownEvent精确控制任务完成的详细内容,更多关于C#并发编程的资料请关注本站其它相关文章!
相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表