C# - 多线程
C# - 现代并发与多线程
Section titled “C# - 现代并发与多线程”在现代软件开发中,并发是指多个任务看似同时执行的能力。线程是进程内执行的基本单位。通过使用多线程,应用程序可以同时执行多项工作,从而提高响应能力和性能,尤其对于带有用户界面 (UI) 或执行耗时输入/输出 (I/O) 操作(如网络请求或文件访问)的应用程序。
尽管 C# 从一开始就支持低级线程管理,但现代方法围绕着任务并行库 (TPL) 提供的高级抽象。使用 Task、async 和 await 等构造简化了并发编程,减少了样板代码,并有助于避免常见的陷阱,如竞态条件和死锁。
从线程到任务:现代方法
Section titled “从线程到任务:现代方法”直接管理 System.Threading.Thread 对象现在被认为是低级操作,仅限于特定场景。对于大多数应用程序级别的并发,您应该使用 Task 类。Task 表示一个可能在单独线程上运行的异步操作。.NET 运行时 使用线程池高效地管理底层线程。
Task 的生命周期比原始线程更抽象,并通过其状态进行管理:
- Created (已创建): 任务已创建但尚未安排运行。
- WaitingToRun (等待运行): 任务已安排执行但正在等待调度程序分配资源。
- Running (正在运行): 任务当前正在执行。
- RanToCompletion (已成功完成): 任务成功完成执行。
- Faulted (故障): 任务因未处理的异常而终止。
- Canceled (已取消): 任务在完成之前被取消。
主线程和 async 主方法
Section titled “主线程和 async 主方法”每个 C# 应用程序都从主线程开始。对于现代 C#(7.1 及更高版本),Main 方法可以标记为 async,允许您在应用程序的顶层使用 await。这是编写执行异步操作的现代控制台应用程序的标准方式。
示例:异步主方法
Section titled “示例:异步主方法”以下程序演示了一个 async Main 方法以及如何启动一个新任务。
using System;using System.Threading.Tasks;
namespace ModernConcurrencyApp { class Program { // Main 方法现在是异步的,并返回一个 Task static async Task Main(string[] args) { Console.WriteLine("主线程已启动。");
// 使用 Task.Run 启动一个后台任务 Task backgroundTask = Task.Run(() => { Console.WriteLine("后台任务正在启动..."); // 模拟 5 秒的工作负载 Task.Delay(5000).Wait(); Console.WriteLine("后台任务已完成。"); });
Console.WriteLine("主线程在后台任务运行时继续执行。");
// 异步等待后台任务完成 await backgroundTask;
Console.WriteLine("后台任务完成后,主线程已完成。"); } }}编译并执行上述代码后,输出将演示非阻塞执行:
Main thread started.Main thread continues execution while the background task runs.Background task starting...(after 5 seconds)Background task finished.Main thread finished after background task completed.关键 Task 和 Task<T> 方法
Section titled “关键 Task 和 Task<T> 方法”以下是一些与 Task 类一起使用的最常用方法:
| 方法 | 描述 |
|---|---|
Task.Run(Action) | 将工作排队到线程池中运行,并返回该工作的 Task 句柄。 |
Task.Delay(int) | 创建一个在指定时间间隔后完成的任务。是 Thread.Sleep 的非阻塞版本。 |
Task.Wait() | 阻塞调用线程直到任务完成。请谨慎使用,因为它可能在 UI 或 ASP.NET Core 上下文中导致死锁。推荐使用 await。 |
await myTask | await 运算符暂停当前方法的执行,直到等待的任务完成。它不会阻塞线程。 |
Task.WhenAll(...) | 创建一个任务,当所有提供的任务都完成后才完成。 |
Task.WhenAny(...) | 创建一个任务,当任何提供的任务完成后即完成。 |
传递参数和返回值
Section titled “传递参数和返回值”使用 lambda 表达式是向任务传递参数的标准方式。要返回一个值,请使用 Task<TResult>,其中 TResult 是您希望返回值的类型。可以通过 await 任务来检索结果。
示例:带参数和返回值的任务
Section titled “示例:带参数和返回值的任务”此示例演示了一个接受参数并返回结果的任务。
using System;using System.Threading.Tasks;
class Program { static async Task Main() { Console.WriteLine("主方法已启动。");
// 使用 lambda 表达式传递参数并运行任务。 // 任务返回一个字符串,因此我们使用 Task<string>。 Task<string> fetchTask = Task.Run(() => FetchDataFromServer("user-123"));
Console.WriteLine("正在等待数据获取...");
// await 任务以获取结果,而不会阻塞线程。 string result = await fetchTask;
Console.WriteLine($"收到的数据: {result}"); Console.WriteLine("主方法已完成。"); }
static string FetchDataFromServer(string userId) { // 模拟网络请求 Task.Delay(3000).Wait(); return $"{{'name': 'John Doe', 'id': '{userId}'}}"; // 返回一个 JSON 类似字符串 }}输出将显示 3 秒延迟后打印结果:
Main method started.Waiting for data to be fetched...(after 3 seconds)Data received: {'name': 'John Doe', 'id': 'user-123'}Main method finished.协作式取消:停止任务的现代方式
Section titled “协作式取消:停止任务的现代方式”旧的 Thread.Abort() 方法很危险,因为它可以在任何时候终止线程,可能导致应用程序状态损坏。它现在已弃用。处理取消的现代、安全方式是使用 CancellationToken 进行协作式取消。
该过程包括:
- 创建
CancellationTokenSource。 - 将其
CancellationToken传递给任务。 - 在任务内部,通过
token.IsCancellationRequested定期检查是否已请求取消。 - 调用代码通过调用
cancellationTokenSource.Cancel()请求取消。
示例:取消任务
Section titled “示例:取消任务”using System;using System.Threading;using System.Threading.Tasks;
class Program { static async Task Main() { var cts = new CancellationTokenSource(); Console.WriteLine("正在启动一个长期运行的任务..."); Console.WriteLine("按 'c' 取消。");
// 启动可取消任务 Task longRunningTask = CountToInfinity(cts.Token);
// 监听用户输入以取消 if (Console.ReadKey().KeyChar == 'c') { cts.Cancel(); Console.WriteLine("\n已请求取消。"); }
try { // 等待任务完成或被取消 await longRunningTask; } catch (OperationCanceledException) { Console.WriteLine("任务已成功取消。"); }
Console.WriteLine("程序已完成。"); }
static async Task CountToInfinity(CancellationToken token) { long i = 0; while (true) { // 在每次迭代中检查取消请求 token.ThrowIfCancellationRequested();
Console.Write($"\r计数: {i++}"); // await 短暂延迟,以防止循环阻塞并保持响应 await Task.Delay(100, token); } }}运行此程序时,它将开始计数。如果您按下“c”键,它将停止并打印取消消息。
这是对 C# 现代并发的简要介绍。对于实际应用程序,您还应该探索:
IProgress<T>: 用于从后台任务向 UI 线程报告进度。- 数据同步:
lock、SemaphoreSlim和并发集合 (Concurrent collections),用于安全地从多个线程访问共享数据。 - 并行 LINQ (PLINQ): 用于轻松地并行化数据处理查询。