C# 多线程并行编程示例:Task 异步执行与主线程同步
本文提供了一个完整的C#多线程并行编程示例,演示如何利用Task类创建并运行多个并发任务,同时介绍如何确保主程序在后台任务全部完成后才优雅退出,避免进程提前终止导致的部分日志丢失问题。
目标与原理
在开发桌面或服务器端应用时,经常需要执行耗时操作而不阻塞用户界面或主流程。C#中的 System.Threading.Tasks.Task 是基于线程池的强大抽象。本示例旨在展示:
- 如何通过匿名委托启动轻量级后台任务。
- 如何处理多线程环境下的资源竞争(虽然本例为只读演示)。
- 如何同步等待任务完成以确保程序正常关闭。
准备条件
- 开发环境:Visual Studio 2019 或更高版本(支持 .NET Framework 4.5+ 或 .NET Core/5/6/7/8)。
- 项目类型:C# 控制台应用程序(Console App)。
- 核心依赖:需引入
System,System.Collections.Generic,System.Linq,System.Threading,System.Threading.Tasks命名空间。
第1步:定义耗时模拟方法
为了体现并行效果,我们需要一个能产生“等待”行为的方法。以下代码模拟了一个带有消息标识和循环间隔的任务逻辑。
using System;
using System.Threading;
namespace ParallelTest
{
class Program
{
// 模拟一个带日志的耗时操作
private static int Timed_Message(string arg_Message, int arg_Interval)
{
for (int i = 0; i < 10; i++)
{
// 向控制台写入当前状态
Console.WriteLine("Source {0} - Cycle {1} for Interval {2}", arg_Message, i, arg_Interval);
// 模拟耗时工作,单位为秒
Thread.Sleep(1000 * arg_Interval);
}
Console.WriteLine("{0} - Complete", arg_Message);
return 0;
}
}
}
该方法的目的是:
1. 打印带有来源标识 arg_Message 的周期性日志。
2. 通过 Thread.Sleep 控制每个周期的间隔时间 arg_Interval。
3. 循环10次后标记该特定任务源已完成。
第2步:启动并行 Task 任务
在 Main 方法中,我们不使用传统的 Thread 类,而是使用更高级别的 Task API来管理并发。这允许我们利用 .NET 的线程池优化性能。
static void Main(string[] args)
{
int RetCode = 0;
// 任务1:每隔4秒打印一次消息 "Five"
// 注意:直接传递闭包引用局部变量 RetCode 需注意作用域,此处仅为演示结构
Task taskOne = new Task(() => RetCode = Timed_Message("Five ", 4));
taskOne.Start();
// 任务2:每隔2秒打印一次消息 "Three"
Task taskTwo = new Task(() => RetCode = Timed_Message("Three ", 2));
taskTwo.Start();
// ... (后续步骤见下文) ...
}
在此处,我们启动了两个不同的后台任务:
- taskOne:对应参数 ("Five ", 4),意味着每轮延迟4秒。
- taskTwo:对应参数 ("Three ", 2),意味着每轮延迟2秒。
这两个任务将在不同的线程上几乎同时开始执行,体现了并行性。
第3步:同步等待与程序退出
如果不在 Main 方法末尾进行干预,当主线程运行结束时,JIT编译器和操作系统可能会强制杀掉正在运行的子任务。为了避免这种情况,必须显式地等待它们完成。
// 等待用户输入以保持在前台观察结果
Console.WriteLine("Press enter to finish after both [Complete] messages appear.");
Console.ReadLine();
}
这里使用了 Console.ReadLine() 作为简易的同步屏障。在实际的生产环境中,推荐使用 Task.WaitAll(taskOne, taskTwo) 或者更具现代感的 await Task.WhenAll(...) (如果使用的是 async/await 模式),以确保在主线程退出前所有后台逻辑均已安全落地。
运行结果分析
编译并运行上述代码后,控制台将混合输出两个任务的日志。由于是两个独立的线程交替获取 CPU 时间片,输出顺序可能因系统负载而异,但遵循以下规律:
- 交错输出:你会看到
Five和Three的消息混杂在一起。例如:Five - Cycle 0之后可能是Three - Cycle 0接着是Three - Cycle 1。 - 速度差异:因为
Three的间隔是2秒而Five是4秒,所以在相同时间段内,Three会产出更多的日志行。 - 结束顺序:较短间隔的任务通常会先打印
[SourceName] - Complete标志。
典型输出片段如下:
Press enter to finish after both [Complete] messages appear. Source Five - Cycle 0 for Interval 4 Source Three - Cycle 0 for Interval 2 Source Three - Cycle 1 for Interval 2 Source Five - Cycle 1 for Interval 4 ... (中间省略部分输出) ... Three - Complete Five - Complete
关键注意事项
- 异常处理:在真实应用中,若后台任务抛出未捕获异常,默认行为可能会导致整个进程崩溃。建议使用
try-catch块包裹任务逻辑或使用Task.ContinueWith来处理错误回调。 - 共享数据保护:本例中
RetCode虽然在多任务间被修改,但由于最后仅用于赋值且未参与复杂计算,故未加锁。涉及写操作的变量时应使用Interlocked类或lock语句以防止竞态条件。 - CancellationToken:长时间运行的任务应支持取消令牌(
CancellationToken),以便外部可以主动终止挂起的操作。
总结
通过此示例,我们掌握了使用 System.Threading.Tasks.Task 实现基础并行处理的模式。这对于提升 IO 密集型或 CPU 密集型批处理任务的效率非常有效。以上就是 C# 并行编程中使用 Task 进行多线程异步处理的详细内容,更多关于 C# 并发编程进阶的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













