C# CountdownEvent 类 使用详解
总目录
前言
CountdownEvent
是 C# 中用于多线程协作的同步工具,位于 System.Threading 命名空间下。它提供了一种简单而有效的方式来等待多个并发操作完成。CountdownEvent 的核心思想是初始化一个计数器,在每个操作完成时减少该计数器,并在计数器归零时释放所有等待的线程。适用于需要等待多个并行操作完成的场景(例如分阶段任务、批量数据处理)。
一、核心概念
- 计数器机制:初始化时指定一个初始计数值,每次调用
Signal()
方法减少计数器。当计数器归零时,所有等待的线程被唤醒。- 初始化计数器:通过构造函数或 InitialCount 属性设置初始计数值。
- 递减计数器:每次调用 Signal() 方法时,计数器会递减。
- 等待计数器归零:调用 Wait() 方法的线程会被阻塞,直到计数器变为0。
- 手动重置:可以通过调用 Reset() 方法将计数器重置为任意值,以便重复使用。
- 适用场景:
- 等待一组并行任务全部完成的场景,如并行计算、多任务处理等。
- 分阶段任务的协调(如“所有子任务完成后进入下一阶段”)。
- 与
Task.WaitAll
的区别:CountdownEvent
更灵活,支持动态调整计数器。Task.WaitAll
仅适用于Task
对象,而CountdownEvent
可与任何线程模型结合。
二、基本用法
1. 构造函数
var countdownEvent = new CountdownEvent(initialCount: 3); // 初始计数值为3
2. 关键方法
方法 | 作用 |
---|---|
Signal() | 减少计数器(原子操作),表示一个任务已经完成 返回 true 表示成功。若计数器已归零,返回 false 。 |
Wait() | 阻塞当前线程,直到计数器归零。 |
AddCount(int value) | 手动增加计数器(需确保当前未处于终止状态)。 |
Reset() | 重置计数器(需谨慎使用,可能破坏协作逻辑)。 |
Dispose() | 释放资源。 |
CurrentCount | 获取当前计数器的值。 |
InitialCount | 获取计数器的初始值。 |
三、示例
示例 1:等待多个任务完成
using System.Threading;
class Program
{
static CountdownEvent countdown = new CountdownEvent(3); // 初始计数值3
static void Main()
{
// 启动3个并行任务
new Thread(DoWork).Start("Task 1");
new Thread(DoWork).Start("Task 2");
new Thread(DoWork).Start("Task 3");
countdown.Wait(); // 阻塞直到所有任务完成
Console.WriteLine("所有任务已完成!");
}
static void DoWork(object name)
{
Thread.Sleep(1000);
Console.WriteLine($"{name} 完成");
countdown.Signal(); // 减少计数器
}
}
输出:
Task 1 完成
Task 3 完成
Task 2 完成
所有任务已完成!
示例 2:动态调整计数器
static CountdownEvent countdown = new CountdownEvent(2);
static void Main()
{
// 初始需要等待2个信号
new Thread(() => {
Thread.Sleep(500);
Console.WriteLine("任务A完成");
countdown.Signal();
}).Start();
// 动态增加计数器(需确保未处于终止状态)
countdown.AddCount(1); // 总计数器变为3
new Thread(() => {
Thread.Sleep(1000);
Console.WriteLine("任务B完成");
countdown.Signal();
}).Start();
new Thread(() => {
Thread.Sleep(1500);
Console.WriteLine("任务C完成");
countdown.Signal();
}).Start();
countdown.Wait();
Console.WriteLine("所有任务完成");
}
输出:
任务A完成
任务B完成
任务C完成
所有任务完成
示例3:最佳实践-等待多个线程完成
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
int taskCount = 5;
CountdownEvent countdown = new CountdownEvent(taskCount);
Console.WriteLine("启动多个任务...");
for (int i = 1; i <= taskCount; i++)
{
int taskId = i;
Task.Run(() =>
{
try
{
Console.WriteLine($"任务 {taskId} 开始执行...");
Thread.Sleep(1000); // 模拟任务执行时间
Console.WriteLine($"任务 {taskId} 完成");
}
finally
{
countdown.Signal(); // 标记任务完成
}
});
}
Console.WriteLine("主线程等待所有任务完成...");
countdown.Wait(); // 等待所有任务完成
Console.WriteLine("所有任务已完成");
}
}
四、注意事项
1. 资源释放
- 使用
Dispose()
或using
块释放资源,避免句柄泄漏。 - 在实际应用中,建议在 Signal() 调用周围添加 try-finally 块,以确保即使在任务抛出异常的情况下也能正确地递减计数器。
using (var countdown = new CountdownEvent(3))
{
// 使用 countdown
}
2. 线程安全
Signal()
和AddCount()
是线程安全的,但需确保逻辑正确性。- 避免在计数器归零后继续调用
Signal()
或AddCount()
(可能抛出InvalidOperationException
)。
3. 动态调整的陷阱
- 调用
AddCount()
时需确保事件未终止(即计数器未归零),否则会抛出异常。 - 示例错误代码:
var countdown = new CountdownEvent(1); countdown.Signal(); // 计数器归零 countdown.AddCount(1); // 抛出 InvalidOperationException
五、高级用法
1. 超时等待
bool completed = countdown.Wait(TimeSpan.FromSeconds(5));
if (!completed)
{
Console.WriteLine("等待超时,仍有未完成的任务");
}
2. 结合异步编程
async Task RunAsync()
{
var countdown = new CountdownEvent(3);
var tasks = new Task[3];
for (int i = 0; i < 3; i++)
{
tasks[i] = Task.Run(() =>
{
// 模拟工作
Thread.Sleep(1000);
countdown.Signal();
});
}
await Task.Run(() => countdown.Wait());
Console.WriteLine("所有异步任务完成");
}
六、常见问题
1. 问题:忘记调用 Signal()
- 现象:主线程永久阻塞在
Wait()
。 - 解决:确保每个子任务正确调用
Signal()
。
2. 问题:计数器归零后继续操作
- 现象:调用
AddCount()
或Signal()
抛出异常。 - 解决:检查计数器状态,或使用
TryAddCount()
方法:if (countdown.TryAddCount(1)) { // 成功增加计数器 }
七、替代方案
Task.WhenAll
:更适合纯Task
模型的异步操作。Barrier
:适用于分阶段的多线程协作(如循环执行多个阶段)。SemaphoreSlim
:控制并发线程数量,但逻辑不同。
结语
回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
xxx