mi-assessment/docs/开发规范/1-编程规约/1.4-多线程与异步规范.md
2026-02-03 20:50:51 +08:00

185 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<string> 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<string, User> _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. **不可变性**:尽可能设计不可变对象,天然线程安全。