# 1.4 多线程与异步规范 本规范旨在指导 .NET 中的多线程与异步编程,确保代码的高效性、稳定性和可维护性,避免常见的死锁、线程饥饿和竞态条件问题。 ## 1. 异步编程 (Async/Await) ### 1.1 避免 `async void` * **规则**:严禁使用 `async void`,唯一的例外是事件处理程序 (Event Handlers)。 * **理由**: * `async void` 方法无法被等待 (await)。 * `async void` 方法中抛出的异常无法被调用方捕获,会导致进程崩溃(除非在 SynchronizationContext 中捕获)。 * 难以测试和组合。 * **反例** : ```csharp // ❌ [不推荐] 使用 async void,异常无法被外部捕获 private async void ExecuteAsync(object obj) { try { await _jobPerformer.Perform(...); } catch (Exception e) { // 必须在内部吞掉所有异常,否则进程崩溃 Console.WriteLine(e); } } ``` * **正例**: ```csharp // ✅ [推荐] 返回 Task,允许调用方等待和处理异常 private async Task ExecuteAsync(object obj) { try { await _jobPerformer.Perform(...); } catch (Exception e) { _logger.LogError(e, "Error occurred"); } } ``` ### 1.2 异步全链路 (Async All the Way) * **规则**:一旦开始使用异步,应在整个调用链路中保持异步。 * **理由**:避免同步/异步混合导致的死锁(Sync-over-Async)和线程池饥饿。 * **反例**: ```csharp // ❌ [禁止] 在异步方法中阻塞等待 public void DoSomething() { DoSomethingAsync().Result; // 或者 .Wait() } ``` * **正例**: ```csharp // ✅ [推荐] 使用 await public async Task DoSomething() { await DoSomethingAsync(); } ``` ### 1.3 库代码使用 `ConfigureAwait(false)` * **规则**:在通用类库 (非 UI 层、非 ASP.NET Core Controller 层) 代码中,应使用 `.ConfigureAwait(false)`。 * **理由**:避免在不需要特定 `SynchronizationContext` (如 UI 线程) 的情况下强制切回原上下文,提高性能并减少死锁风险。 * **注意**:在 ASP.NET Core 应用层代码中通常不需要,因为 ASP.NET Core 没有 SynchronizationContext。但在编写底层 SDK (如 `Aegis.Caching`) 时必须遵守。 * **正例**: ```csharp // ✅ [推荐] 类库代码 public async Task GetDataAsync() { var result = await _httpClient.GetStringAsync(url).ConfigureAwait(false); return result; } ``` ### 1.4 传入 CancellationToken * **规则**:异步方法应尽可能支持 `CancellationToken`,以便在操作不再需要时取消。 * **正例**: ```csharp // ✅ [推荐] 支持取消 public async Task ProcessAsync(CancellationToken cancellationToken = default) { while (!cancellationToken.IsCancellationRequested) { await Task.Delay(1000, cancellationToken); // ... 业务逻辑 } } ``` ## 2. 线程与任务 (Threads & Tasks) ### 2.1 优先使用 Task 而非 Thread * **规则**:使用 `Task.Run` 或 `Task.Factory.StartNew` (带有 `TaskCreationOptions.LongRunning`),尽量避免直接 `new Thread()`。 * **理由**:`Task` 基于线程池,资源利用率更高,且 API 更易于组合和异常处理。 * **例外**:需要设置线程优先级、名称或必须是前台线程的特殊场景。 ### 2.2 避免长时间阻塞线程池线程 * **规则**:如果任务是 CPU 密集型且运行时间很长,应指定 `TaskCreationOptions.LongRunning`,或者使用独立的线程,避免耗尽线程池导致吞吐量下降。 ## 3. 线程安全与锁 (Locking) ### 3.1 锁的选择 * **规则**: * **同步代码**:首选 `lock` (即 `Monitor`)。 * **异步代码**:必须使用 `SemaphoreSlim`,严禁在 `lock` 块中使用 `await`。 * **高性能/轻量级**:`Interlocked` 用于简单的计数器或原子交换。 * **自旋锁**:慎用 `SpinLock` 或 `SpinWait`,除非非常短的临界区且你非常清楚自己在做什么。 *建议:对于大多数业务场景,普通的 `lock` 或 `SemaphoreSlim` 足够且更安全。除非经过 Benchmark 证明必须使用自旋锁。* * **正例 (异步锁)**: ```csharp private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); public async Task SafeOperationAsync() { await _lock.WaitAsync(); try { // ✅ 可以在锁内 await await DoSomethingAsync(); } finally { _lock.Release(); } } ``` ### 3.2 线程安全的集合 * **规则**:在多线程环境下读写集合,应使用 `System.Collections.Concurrent` 命名空间下的集合 (如 `ConcurrentDictionary`, `ConcurrentQueue`),而不是手动加锁的 `List` 或 `Dictionary`。 * **正例**: ```csharp private readonly ConcurrentDictionary _cache = new(); public void AddUser(User user) { // ✅ 线程安全 _cache.TryAdd(user.Id, user); } ``` ### 3.3 静态成员的线程安全 * **规则**:静态成员 (Static Members) 在多线程环境下是共享的,必须保证其线程安全。 * **建议**: * 尽量设计为不可变 (Immutable)。 * 如果必须可变,使用 `lock` 或 `Concurrent` 类型保护。 ## 4. 并行处理 (Parallel) ### 4.1 控制并发度 * **规则**:使用 `Parallel.ForEach` 或 `Task.WhenAll` 时,必须控制并发数量 (`MaxDegreeOfParallelism`),防止瞬间并发过高压垮下游服务 (数据库、Redis、外部 API)。 * **正例**: ```csharp var options = new ParallelOptions { MaxDegreeOfParallelism = 5 }; await Parallel.ForEachAsync(items, options, async (item, token) => { await ProcessItemAsync(item, token); }); ``` ### 4.2 避免 Parallel 里的共享状态写入 * **规则**:避免在并行循环中写入共享变量,这通常是非线程安全的。应使用线程局部变量或并发集合。 ## 5. 最佳实践总结 1. **不要阻塞异步代码**:避免 `.Result`, `.Wait()`。 2. **异常处理**:`Task` 中的异常会被包装在 `AggregateException` 中 (如果使用 `.Result` 或 `.Wait()`),但在 `await` 时会解包抛出第一个异常。建议始终使用 `await`。 3. **避免线程饥饿**:不要在线程池线程中执行长时间的同步 IO 操作。 4. **不可变性**:尽可能设计不可变对象,天然线程安全。