C# 并发与多线程:进阶篇
\n\n在现代 C# (.NET) 开发中,高效管理并发与多线程的能力是区分资深工程师与新手的标志。仅凭 Thread 和简单锁的基础知识,已不足以构建高性能、响应迅速且可扩展的应用程序。本文将深入探讨进阶技术:从深刻理解 async/await 到使用 System.Threading.Channels 和 TPL Dataflow。本文面向已掌握多线程基础的开发者。
1. 深入理解 async/await 与 SynchronizationContext
\n\n许多开发者使用 async/await,但并未完全理解任务调度器 (Task Scheduler) 和 SynchronizationContext 如何管理延续 (continuations)。在进阶层面,控制 await 之后代码在哪个线程执行至关重要。
默认情况下,await 会捕获当前的 SynchronizationContext(或 TaskScheduler)。在 UI 应用程序(WPF、WinForms)中,这会将执行返回到主线程。在 ASP.NET Core 和控制台应用程序中,上下文通常不存在,延续可能会在线程池中的任意线程上执行。
如需精细控制,请使用 ConfigureAwait(false)。这会告知运行时不要尝试返回到原始上下文,从而提高性能并防止库代码中的死锁。
public async Task<string> FetchDataAsync(HttpClient client)\n{\n // 延续不会捕获上下文\n var result = await client.GetStringAsync("https://api.example.com")\n .ConfigureAwait(false);\n // 此代码将在线程池中的任意线程上执行\n return ProcessResult(result);\n}\n\npublic async Task<string> FetchDataWithContextAsync(HttpClient client)\n{\n // 延续将返回到原始上下文(例如 UI 线程)\n var result = await client.GetStringAsync("https://api.example.com");\n // 此代码保证在原始上下文中执行\n return ProcessResult(result);\n}\n\n进阶建议: 为非常规场景创建自定义 SynchronizationContext,例如组织具有固定线程数的任务队列。
2. 任务并行库 (TPL) 与数据并行
\n\nTPL 为并行提供了高级抽象。Parallel.For 和 Parallel.ForEach 会自动将集合拆分为块 (chunks) 并分配到线程上。然而,在进阶层面,需要理解如何配置并行度以及异常处理。
使用 ParallelOptions 限制线程数,并使用 ParallelLoopState 提前退出循环。
var options = new ParallelOptions\n{\n MaxDegreeOfParallelism = Environment.ProcessorCount / 2 // 使用一半的核心\n};\n\nParallel.ForEach(sourceData, options, (item, state) =>\n{\n try\n {\n // 耗时操作\n ProcessItem(item);\n }\n catch (Exception ex)\n {\n // 记录日志并中断执行\n Console.WriteLine($"错误: {ex.Message}");\n state.Stop(); // 中断循环执行\n }\n});\n\n重要提示: Parallel.ForEach 不保证处理顺序。如果顺序很重要,请使用带索引的 Parallel.ForEach 或 PLINQ (AsParallel().AsOrdered())。
3. 通道 (System.Threading.Channels) 用于生产者-消费者模式
\n\nChannel<T> 类自 .NET Core 3.0 引入,已成为生产者-消费者场景的强大工具。它是线程安全、异步且高效的。通道分为