Skip to content

C# - 多线程

在现代软件开发中,并发是指多个任务看似同时执行的能力。线程是进程内执行的基本单位。通过使用多线程,应用程序可以同时执行多项工作,从而提高响应能力和性能,尤其对于带有用户界面 (UI) 或执行耗时输入/输出 (I/O) 操作(如网络请求或文件访问)的应用程序。

尽管 C# 从一开始就支持低级线程管理,但现代方法围绕着任务并行库 (TPL) 提供的高级抽象。使用 Task、async 和 await 等构造简化了并发编程,减少了样板代码,并有助于避免常见的陷阱,如竞态条件和死锁。

直接管理 System.Threading.Thread 对象现在被认为是低级操作,仅限于特定场景。对于大多数应用程序级别的并发,您应该使用 Task 类。Task 表示一个可能在单独线程上运行的异步操作。.NET 运行时 使用线程池高效地管理底层线程。

Task 的生命周期比原始线程更抽象,并通过其状态进行管理:

  • Created (已创建): 任务已创建但尚未安排运行。
  • WaitingToRun (等待运行): 任务已安排执行但正在等待调度程序分配资源。
  • Running (正在运行): 任务当前正在执行。
  • RanToCompletion (已成功完成): 任务成功完成执行。
  • Faulted (故障): 任务因未处理的异常而终止。
  • Canceled (已取消): 任务在完成之前被取消。

每个 C# 应用程序都从主线程开始。对于现代 C#(7.1 及更高版本),Main 方法可以标记为 async,允许您在应用程序的顶层使用 await。这是编写执行异步操作的现代控制台应用程序的标准方式。

以下程序演示了一个 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.Run(Action)将工作排队到线程池中运行,并返回该工作的 Task 句柄。
Task.Delay(int)创建一个在指定时间间隔后完成的任务。是 Thread.Sleep 的非阻塞版本。
Task.Wait()阻塞调用线程直到任务完成。请谨慎使用,因为它可能在 UI 或 ASP.NET Core 上下文中导致死锁。推荐使用 await。
await myTaskawait 运算符暂停当前方法的执行,直到等待的任务完成。它不会阻塞线程。
Task.WhenAll(...)创建一个任务,当所有提供的任务都完成后才完成。
Task.WhenAny(...)创建一个任务,当任何提供的任务完成后即完成。

使用 lambda 表达式是向任务传递参数的标准方式。要返回一个值,请使用 Task<TResult>,其中 TResult 是您希望返回值的类型。可以通过 await 任务来检索结果。

此示例演示了一个接受参数并返回结果的任务。

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 进行协作式取消。

该过程包括:

  1. 创建 CancellationTokenSource。
  2. 将其 CancellationToken 传递给任务。
  3. 在任务内部,通过 token.IsCancellationRequested 定期检查是否已请求取消。
  4. 调用代码通过调用 cancellationTokenSource.Cancel() 请求取消。
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): 用于轻松地并行化数据处理查询。