185 lines
6.6 KiB
Markdown
185 lines
6.6 KiB
Markdown
# 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. **不可变性**:尽可能设计不可变对象,天然线程安全。
|