写在前面
高并发是后端绕不开的话题。面试问、生产遇、架构要想。.NET 在高并发这块其实很强——async/await 模型、Channel、Span、Kestrel 都是世界级的实现,只是被 Java/Go 的声量盖住了。
本文系统梳理 .NET 处理高并发的全景:从理论概念,到异步编程、同步原语、并发集合、并行计算、高性能内存、缓存、限流熔断,最后落到数据库和监控。读完能建立完整的 .NET 高并发知识体系。
版本说明:本文基于 .NET 8 LTS(代码风格与 API 均适用),最低要求 .NET 7(内置限流中间件 AddRateLimiter 是 .NET 7 引入)。所有特性向上兼容 .NET 9 / .NET 10。涉及的关键 API 引入版本见各章节注释。
一、什么是高并发
1.1 定义
高并发没有绝对标准,通常指系统在短时间内处理大量请求的能力。但"高"是相对的:
1
2
3
4
5
6
7
8
9
10
| 场景 QPS(每秒请求数) 并发连接数
─────────────────────────────────────────────────
个人博客 10 ~ 100 < 100
中型网站 1k ~ 5k 1k ~ 5k
电商秒杀 1万 ~ 10万 1万 ~ 5万
双十一峰值 10万 ~ 百万 十万级
搜索/广告 百万级 百万级
高并发不是单一指标,是 QPS、响应时间、并发数、
错误率、资源占用等多个维度的综合表现。
|
1.2 核心指标
1
2
3
4
5
6
7
8
9
| QPS / TPS — 每秒请求数 / 事务数(吞吐量)
并发数 — 同时处理的请求数
响应时间(RT) — P50 / P95 / P99(看尾部延迟,不只看平均)
错误率 — 失败请求占比
资源占用 — CPU、内存、网络、磁盘 I/O
黄金法则:
吞吐量(QPS)= 并发数 / 平均响应时间
想提高 QPS:要么提高并发数,要么降低响应时间
|
1.3 高并发的挑战
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 1. 资源竞争
多线程访问共享资源 → 需要同步(锁)→ 锁竞争降低性能
2. 数据一致性
并发写同一数据 → 脏读、丢失更新、超卖
3. 性能瓶颈
数据库连接、网络 I/O、CPU、内存都可能成为瓶颈
4. 系统稳定性
流量突增 → 雪崩(一个服务拖垮整个链路)
需要限流、熔断、降级
5. 可观测性
高并发下问题难定位 → 需要链路追踪、指标监控
|
二、概念澄清:并发、并行、异步
这三个词常被混用,但本质不同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 并发(Concurrency)— 同时应对多件事(可能交替执行)
一个厨师同时做三道菜:切菜→炒A→切肉→炖汤→炒B...
单核 CPU 也能并发(时间片轮转)
并行(Parallelism)— 同时做多件事(真正同时)
三个厨师同时做三道菜
需要多核 CPU 才能真正并行
异步(Asynchronous)— 不等待,先干别的
厨师把菜放进烤箱,趁这时间去切菜,烤箱好了再回来
本质是"不阻塞等待 I/O"
关系:
异步是实现并发的一种方式(不阻塞线程)
并行是利用多核同时计算
高并发系统通常 = 异步 I/O + 适度并行 + 缓存 + 限流
|
1
2
3
4
| .NET 中的对应:
异步 → async/await、Task(处理 I/O 等待)
并行 → Parallel.For、PLINQ(利用多核计算)
并发 → 并发集合、Channel(多线程协作)
|
三、异步编程:async/await(.NET 的招牌)
async/await 是 .NET 高并发的基石。它让你写出看起来同步、实际异步的代码,不阻塞线程。
3.1 为什么异步能扛高并发
1
2
3
4
5
6
7
8
9
10
11
12
13
| 同步代码:
请求1:读数据库(线程等 50ms)→ 处理 → 返回
请求2:读数据库(线程等 50ms)→ 处理 → 返回
每个请求占用一个线程,线程在等 I/O 时空转
线程池默认几十个线程 → 几十个并发就饱和
异步代码:
请求1:发起读数据库(线程释放)→ I/O 完成回调 → 处理 → 返回
请求2:发起读数据库(线程释放)→ ...
等 I/O 时不占线程,少量线程就能处理上万并发
这就是 Kestrel 单机能扛数十万并发的根本原因
|
3.2 基本用法
1
2
3
4
5
6
7
8
9
10
11
| // 同步(阻塞线程)
public User GetUser(int id)
{
return _db.Users.Find(id); // 线程在这里等数据库
}
// 异步(不阻塞线程)
public async Task<User> GetUserAsync(int id)
{
return await _db.Users.FindAsync(id); // 等待时线程释放
}
|
3.3 async/await 的本质:状态机
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 你写的代码
public async Task<string> GetDataAsync()
{
var a = await GetDataAAsync();
var b = await GetDataBAsync();
return a + b;
}
// 编译器生成的(简化)
// 本质是一个状态机,await 之间是状态切换
public Task<string> GetDataAsync()
{
var stateMachine = new StateMachine();
stateMachine.builder = AsyncTaskMethodBuilder<string>.Create();
stateMachine.MoveNext(); // 执行到第一个 await
return stateMachine.builder.Task;
}
// MoveNext 内部根据状态决定执行哪一段
// state 0: 调 GetDataAAsync,注册回调,return
// state 1: A 完成,调 GetDataBAsync,注册回调,return
// state 2: B 完成,拼接结果,完成 Task
|
1
2
3
4
| 关键理解:
await 不是"阻塞等待",而是"注册回调后返回"
线程在 await 处释放,I/O 完成后用线程池线程继续执行后续代码
这就是为什么异步不占线程
|
3.4 Task vs ValueTask
1
2
3
4
5
6
7
8
9
10
11
12
13
| // Task — 引用类型,每次都分配对象
public async Task<int> CountAsync()
{
if (_cached) return _value; // 即使同步返回,也分配了 Task
return await ComputeAsync();
}
// ValueTask — 结构体,热路径可避免分配
public ValueTask<int> CountAsync()
{
if (_cached) return new ValueTask<int>(_value); // 零分配!
return new ValueTask<int>(ComputeAsync());
}
|
1
2
3
4
5
6
7
8
| 选择:
Task — 一般场景,API 简单,默认用它
ValueTask — 高频调用、可能同步完成的热路径
(缓存命中、内存计算)
注意:
ValueTask 只能 await 一次(不能多次 await)
不确定就用 Task,性能敏感再上 ValueTask
|
3.5 CancellationToken(取消)
高并发系统必须有取消机制——超时、用户放弃、服务降级时及时停止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public async Task<User> GetUserAsync(int id, CancellationToken cancellationToken)
{
// 传递给下游
return await _db.Users.FindAsync(id, cancellationToken);
}
// 调用方控制取消
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
var user = await GetUserAsync(1, cts.Token);
}
catch (OperationCanceledException)
{
// 超时或主动取消
}
// 手动取消
cts.Cancel();
|
1
2
3
4
5
| 最佳实践:
✓ 所有公开的异步方法都接受 CancellationToken
✓ 传递给下游(数据库、HTTP、延时)
✓ 在循环里检查 token.ThrowIfCancellationRequested()
✗ 不要吞掉 CancellationToken 参数
|
1
2
3
4
5
| // 库代码:不要捕获同步上下文(避免死锁、提升性能)
await DoSomethingAsync().ConfigureAwait(false);
// ASP.NET Core:没有同步上下文,ConfigureAwait(false) 无意义
// (但写了也没坏处,库代码建议写)
|
1
2
3
4
5
6
7
| 历史背景:
老的 ASP.NET / WinForms / WPF 有 SynchronizationContext
await 默认回到原上下文 → 可能死锁
ConfigureAwait(false) 避免
ASP.NET Core 没有 SynchronizationContext → 不存在这问题
但写库时养成习惯,跨平台兼容
|
3.7 IAsyncEnumerable(异步流)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 异步流:边产生边消费,不用一次性全部加载
public async IAsyncEnumerable<User> GetUsersAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var row in _db.Users.AsAsyncEnumerable().WithCancellation(ct))
{
yield return row; // 产生一个就返回一个
}
}
// 消费
await foreach (var user in GetUsersAsync())
{
Process(user); // 流式处理,内存友好
}
|
四、同步原语
先解释这个词,“同步原语”(Synchronization Primitive)不太好理解,拆开看:
- 同步 — 协调多个线程/进程,让它们有序访问共享资源,避免抢成一团
- 原语 — 最基础、不可分割的操作单元(primitive,“原始的”)
合起来:操作系统/运行时提供的、用来协调多线程访问共享资源的最底层机制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| 为什么需要"原语":
多个线程同时执行 _count++,看似一行,实际是三步:
1. 读 _count 到寄存器
2. 寄存器 +1
3. 写回 _count
线程 A、B 同时执行,可能互相覆盖 → 丢更新
你没法自己"实现"一个原子操作,必须用系统/运行时提供的工具
这些工具就是"原语"——它们是构建复杂同步逻辑的"原子积木"
打个比方:
同步原语 = 乐高的基础积木块(lock、Semaphore、Event...)
Channel、生产者-消费者 = 用积木拼出来的复杂结构
.NET 里的同步原语:
lock / Monitor — 互斥锁
Semaphore / SemaphoreSlim — 信号量
Interlocked — 原子操作
ManualResetEvent 系列 — 事件等待句柄
Mutex / ReaderWriterLockSlim — 互斥锁 / 读写锁
|
当多线程必须访问共享资源时,就用这些原语来协调。下面逐一介绍。
4.1 选择指南
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 场景 推荐
──────────────────────────────────────────────
简单的临界区保护 lock / Monitor
异步代码里的锁 SemaphoreSlim
原子计数/标志 Interlocked
读多写少 ReaderWriterLockSlim
进程内信号量/并发限制 SemaphoreSlim
跨进程互斥 Mutex
跨进程信号量/并发限制 Semaphore
跨进程事件通知 EventWaitHandle
线程间事件通知(进程内) ManualResetEventSlim
等待N个任务完成(fork-join) CountdownEvent
生产者-消费者 Channel<T>(不用自己加锁)
高并发字典 ConcurrentDictionary
|
4.2 lock / Monitor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| private readonly object _lock = new();
private int _count;
public void Increment()
{
lock (_lock) // 等价于 Monitor.Enter / Exit
{
_count++;
}
}
// 注意:
// 1. lock 对象要是 readonly private,别 lock(this)、lock(typeof(X))
// 2. lock 内不要 await(lock 不支持,会编译错误)
// 3. 持锁时间尽量短
|
4.3 SemaphoreSlim(异步友好)
lock 不能 await,异步代码用 SemaphoreSlim。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| private readonly SemaphoreSlim _semaphore = new(1, 1); // 初始1,最大1 = 互斥
public async Task UpdateAsync()
{
await _semaphore.WaitAsync(); // 异步等待锁
try
{
await DoWorkAsync(); // 锁内可以 await
}
finally
{
_semaphore.Release(); // 必须释放
}
}
// 也可以做并发数限制(不只是互斥)
private readonly SemaphoreSlim _concurrencyLimit = new(100); // 最多100并发
|
4.4 Semaphore(跨进程信号量)
SemaphoreSlim 只能进程内,需要跨进程限制资源并发访问时用 Semaphore。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 命名信号量:跨进程可见
// 限制本机最多 3 个进程同时访问某资源
using var semaphore = new Semaphore(
initialCount: 3,
maximumCount: 3,
name: @"Global\MyAppResourceLimit"); // 命名 → 跨进程
semaphore.WaitOne(); // 获取(计数-1),满了就等
try
{
AccessSharedResource(); // 临界区,跨进程生效
}
finally
{
semaphore.Release(); // 释放(计数+1)
}
|
1
2
3
4
5
6
7
8
9
10
11
| Semaphore vs SemaphoreSlim:
Semaphore SemaphoreSlim
─────────────────────────────────────────────────────────
底层实现 OS 内核对象 托管对象(用户态)
异步等待 WaitAsync ✗(只有 WaitOne) ✓
跨进程(命名) ✓ ✗
性能 较低(内核态切换) 高
选择:
进程内 + 异步 → SemaphoreSlim(绝大多数场景)
跨进程限制并发 → Semaphore(命名信号量)
|
1
2
3
4
| 信号量 vs Mutex:
Mutex — 互斥,同一时刻只允许 1 个
Semaphore — 计数,允许 N 个(initialCount 控制)
适合"资源池"场景(限制最多 N 个并发连接/任务)
|
4.5 事件等待句柄(信号通知)
用于线程(或进程)之间的信号通知——一个线程通知另一个线程某事件发生了。
1
2
3
4
5
6
| EventWaitHandle 家族:
EventWaitHandle — 基类,支持命名(跨进程)
ManualResetEvent — 手动重置(继承 EventWaitHandle)
AutoResetEvent — 自动重置(继承 EventWaitHandle)
ManualResetEventSlim — 轻量版,进程内,性能更好
CountdownEvent — 等待 N 个信号(fork-join)
|
ManualResetEvent(闸门)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 像一道闸门:打开后保持打开,所有等待者都通过,直到手动关闭
var gate = new ManualResetEvent(initialState: false); // 初始关闭
// 多个工作线程等待开工信号
for (int i = 0; i < 10; i++)
Task.Run(() => {
gate.WaitOne(); // 阻塞,直到闸门打开
DoWork();
});
// 主线程打开闸门 → 所有等待线程同时放行(广播)
gate.Set();
// 闸门保持打开;需要再次阻塞时手动关闭
gate.Reset();
|
1
2
| 特点:Set 后保持 signaled,所有 WaitOne 立即返回
适合:一次性广播通知(初始化完成、开始信号)
|
AutoResetEvent(旋转门)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 像旋转门:Set 放行一个等待者后,自动关上
var turnstile = new AutoResetEvent(initialState: false);
// 消费者
Task.Run(() => {
while (true)
{
turnstile.WaitOne(); // 等待信号
ProcessItem();
}
});
// 生产者:每来一个就 Set 一次(放行一个)
turnstile.Set();
|
1
2
3
| 特点:Set 释放一个等待者后自动 Reset
适合:一次通知一个(经典生产者-消费者信号)
但现代推荐用 Channel<T>,更强大
|
ManualResetEventSlim(轻量版)
1
2
3
4
5
6
7
8
9
| // 进程内的 ManualResetEvent,性能更好
var slim = new ManualResetEventSlim(initialState: false);
slim.Wait(); // 先自旋一小段,再阻塞(短等待更快)
slim.Set();
slim.Reset();
// 区别:不能跨进程,但短时间等待性能更好
// 进程内场景优先用它
|
CountdownEvent(fork-join)
1
2
3
4
5
6
7
8
9
10
| // 等待 N 个并行任务全部完成
var countdown = new CountdownEvent(5); // 等 5 个信号
Parallel.For(0, 5, i =>
{
DoTask(i);
countdown.Signal(); // 完成 1 个,计数 -1
});
countdown.Wait(); // 等待全部完成
|
1
2
| 适合:分叉-汇合(fork-join)模式
启动 N 个并行任务,等所有完成
|
跨进程事件通知
1
2
3
4
5
6
7
8
| // 命名事件:跨进程可见(两个进程协调)
var evt = new EventWaitHandle(
initialState: false,
mode: EventResetMode.AutoReset,
name: @"Global\MyAppStartSignal");
evt.WaitOne(); // 进程A 等待
evt.Set(); // 进程B 通知
|
1
2
3
4
5
| 现代场景的替代:
异步等待信号 → TaskCompletionSource(把信号变成可 await 的 Task)
生产者-消费者 → Channel<T>
这些等待句柄是阻塞式的,async 场景不太合适
但跨进程协调、遗留代码兼容、特定同步场景仍不可替代
|
4.6 Interlocked(原子操作)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| private int _counter;
// 原子自增(无锁,极快)
Interlocked.Increment(ref _counter);
Interlocked.Decrement(ref _counter);
Interlocked.Add(ref _counter, 10);
// 原子读取/赋值(64位在32位系统上需要)
long value = Interlocked.Read(ref _largeValue);
Interlocked.Exchange(ref _counter, 0);
// CAS(Compare-And-Swap)— 无锁编程基础
int original;
do
{
original = _counter;
} while (Interlocked.CompareExchange(ref _counter, original + 1, original) != original);
|
4.7 ReaderWriterLockSlim(读多写少)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| private readonly ReaderWriterLockSlim _rwLock = new();
// 多个读线程可以同时进入
public User Read(int id)
{
_rwLock.EnterReadLock();
try { return _cache[id]; }
finally { _rwLock.ExitReadLock(); }
}
// 写线程独占
public void Write(int id, User user)
{
_rwLock.EnterWriteLock();
try { _cache[id] = user; }
finally { _rwLock.ExitWriteLock(); }
}
|
五、并发集合与 Channel
多线程协作时,优先用并发集合而不是自己加锁。
5.1 并发集合一览
1
2
3
4
5
6
7
| 集合 特点 适用
──────────────────────────────────────────────────────────────
ConcurrentDictionary 线程安全字典 高并发读写、缓存
ConcurrentQueue<T> 无锁 FIFO 队列 生产者-消费者
ConcurrentStack<T> 无锁 LIFO 栈 工作窃取
ConcurrentBag<T> 无序,线程本地存储 无顺序要求的并行
BlockingCollection<T> 带阻塞的集合(封装上面) 经典生产者-消费者
|
5.2 ConcurrentDictionary
1
2
3
4
5
6
7
8
9
10
11
12
| var cache = new ConcurrentDictionary<string, User>();
// 原子的 GetOrAdd(不存在才添加)
var user = cache.GetOrAdd("key", k => LoadFromDb(k));
// 原子的 AddOrUpdate
cache.AddOrUpdate("key",
addValue: k => new User(),
updateValueFactory: (k, old) => UpdateUser(old));
// 注意:GetOrAdd 的工厂可能被多次调用(非原子)
// 高性能场景用 TryGetValue + TryAdd 手动控制
|
5.3 Channel(.NET 高并发的明星)
System.Threading.Channels 是 .NET 专门为高并发生产者-消费者场景设计的,性能远超 ConcurrentQueue + BlockingCollection。
1
2
3
4
5
6
7
8
| // 安装
// dotnet add package System.Threading.Channels
// 创建 channel(无界)
var channel = Channel.CreateUnbounded<Order>();
// 创建 channel(有界,背压控制)
var bounded = Channel.CreateBounded<Order>(1000); // 最多缓冲 1000
|
生产者
1
2
3
4
5
6
| // 写入
await channel.Writer.WriteAsync(order); // 满了会等待(有界)
channel.Writer.TryWrite(order); // 不等待,返回是否成功
// 完成
channel.Writer.Complete(); // 通知没有更多数据
|
消费者
1
2
3
4
5
6
7
| // 读取
await foreach (var order in channel.Reader.ReadAllAsync())
{
await ProcessAsync(order);
}
// channel 完成且读完时,循环自动结束
|
完整示例:订单处理管道
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| public class OrderPipeline
{
private readonly Channel<Order> _channel;
public OrderPipeline(int capacity = 1000)
{
_channel = Channel.CreateBounded<Order>(new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait, // 满了等待
SingleReader = false,
SingleWriter = false
});
}
// 生产者:接收订单
public async Task EnqueueAsync(Order order, CancellationToken ct)
{
await _channel.Writer.WriteAsync(order, ct);
}
// 消费者:多 worker 并行处理
public async Task ConsumeAsync(int workerCount, CancellationToken ct)
{
var workers = Enumerable.Range(0, workerCount)
.Select(_ => Task.Run(async () =>
{
await foreach (var order in _channel.Reader.ReadAllAsync(ct))
{
await ProcessOrderAsync(order);
}
}));
await Task.WhenAll(workers);
}
public void Complete() => _channel.Writer.Complete();
}
|
1
2
3
4
5
6
7
8
| Channel 的优势:
✓ 无锁或细粒度锁,性能极高
✓ 支持 async/await(不阻塞线程)
✓ 支持背压(BoundedChannel 控制内存)
✓ 支持 SingleReader/SingleWriter 优化
✓ 是 ASP.NET Core 内部用的(SignalR、Quartz 等)
可以说:Channel 是 .NET 替代 Go channel 的方案
|
5.4 不可变集合
1
2
3
4
5
6
7
8
9
| // System.Collections.Immutable
// 每次修改返回新实例,天然线程安全
var list = ImmutableList<int>.Empty;
var list2 = list.Add(1); // list 不变,list2 是新的
var list3 = list2.Add(2);
// 适合:配置、只读缓存、共享状态
// 修改开销大(复制),但读取绝对安全
|
六、并行计算:TPL
CPU 密集型任务要利用多核,用 TPL(Task Parallel Library)。
6.1 Parallel.For / Parallel.ForEach
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // 并行处理(CPU 密集型)
var data = Enumerable.Range(0, 1_000_000).ToArray();
// 并行 For
Parallel.For(0, data.Length, i =>
{
data[i] = Compute(data[i]);
});
// 并行 ForEach
Parallel.ForEach(data, item =>
{
Process(item);
});
// 控制并发度
var options = new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount
};
Parallel.ForEach(data, options, item => Process(item));
|
6.2 PLINQ(并行 LINQ)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // AsParallel() 把 LINQ 变成并行
var result = data
.AsParallel()
.Where(x => x % 2 == 0)
.Select(x => x * x)
.OrderBy(x => x)
.ToList();
// 控制并发度
var result2 = data
.AsParallel()
.WithDegreeOfParallelism(8)
.Select(Compute)
.ToList();
|
1
2
3
4
5
| 注意:
✗ 小数据集别用并行(调度开销 > 收益)
✗ I/O 密集型别用 Parallel/PLINQ(它们是为 CPU 设计的)
I/O 用 Task.WhenAll
✓ 数据量大 + 计算密集才用
|
6.3 Task.WhenAll(并发 I/O)
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 并发请求多个 I/O(不是并行计算,是并发等待)
var urls = new[] { "url1", "url2", "url3" };
// 错误:串行(一个等一个)
var results = new List<string>();
foreach (var url in urls)
{
results.Add(await httpClient.GetStringAsync(url)); // 串行!
}
// 正确:并发(一起发起,一起等)
var tasks = urls.Select(url => httpClient.GetStringAsync(url));
var results = await Task.WhenAll(tasks); // 总耗时 ≈ 最慢的那个
|
1
2
3
4
5
| 区分:
Task.WhenAll — 并发 I/O(等网络,不占 CPU)
Parallel.For — 并行计算(占 CPU 多核)
这是新手最常搞错的点:I/O 密集用 WhenAll,CPU 密集用 Parallel
|
七、高性能内存:Span、Pipelines、Pool
.NET Core 之后引入了一系列高性能内存原语,这是 .NET 能和原生 C++ 掰手腕的资本。
7.1 Span / Memory(零拷贝)
1
2
3
4
5
6
7
8
9
10
11
12
13
| // Span<T> — 连续内存的视图,不复制数据
// 堆栈上分配(ref struct),极快
// 切分数组,零拷贝
byte[] buffer = new byte[1000];
Span<byte> slice = buffer.AsSpan(100, 50); // 从100开始,取50个
// stackalloc — 栈上分配(不进堆,无 GC)
Span<int> stackData = stackalloc int[100]; // 栈上,方法结束自动回收
// 字符串操作零拷贝
string text = "Hello, World";
ReadOnlySpan<char> hello = text.AsSpan(0, 5); // "Hello",不新建字符串
|
1
2
3
4
5
6
7
8
9
10
11
| // 高性能解析示例
// 旧:每步都分配新字符串
var parts = input.Split(','); // 分配数组 + 多个字符串
// 新:用 Span 零拷贝解析
var reader = new SpanReader(input.AsSpan());
while (!reader.Done)
{
var token = reader.ReadUntil(',');
Process(token); // token 是原始数据的切片,不分配
}
|
1
2
3
4
5
6
| Span 限制(ref struct):
✗ 不能作为类的字段
✗ 不能跨 await
✗ 不能装箱
需要跨 await / 存字段 → 用 Memory<T>(堆上,可异步)
|
7.2 System.IO.Pipelines
专门为高性能网络 I/O 设计,Kestrel 内部就用它。解决"数据不完整、粘包"的问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 安装
// dotnet add package System.IO.Pipelines
async Task ProcessPipeAsync(PipeReader reader)
{
while (true)
{
ReadResult result = await reader.ReadAsync();
ReadOnlySequence<byte> buffer = result.Buffer;
// 尝试解析完整消息
while (TryParseMessage(ref buffer, out var message))
{
ProcessMessage(message);
}
// 告诉 PipeReader 消费了多少、还剩多少
reader.AdvanceTo(buffer.Start, buffer.End);
if (result.IsCompleted) break;
}
}
|
1
2
3
4
5
6
| Pipelines 解决的痛点:
✓ 自动管理缓冲区(不用自己 new byte[])
✓ 处理消息不完整(等更多数据)
✓ 处理消息粘包(一次读多条)
✓ 内存复用(零拷贝、少分配)
✓ 背压(消费慢时通知生产者暂停)
|
7.3 ArrayPool / ObjectPool
减少 GC 压力的利器——复用对象而不是反复分配。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // ArrayPool — 复用数组
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024); // 租借(可能更大)
try
{
ProcessData(buffer);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer); // 归还(不是 GC 回收)
}
// ObjectPool — 复用对象(Microsoft.Extensions.ObjectPool)
var pool = new DefaultObjectPool<StringBuilder>(
new DefaultPooledObjectPolicy<StringBuilder>());
var sb = pool.Get();
try
{
sb.Append("...");
var result = sb.ToString();
}
finally
{
sb.Clear();
pool.Return(sb);
}
|
1
2
3
4
5
6
| 何时用 Pool:
✓ 高频分配 + 大对象(大 byte[]、StringBuilder)
✓ 热路径(每秒成千上万次)
✗ 低频场景(开销 > 收益)
原理:对象复用,减少 GC,降低内存分配压力
|
八、线程池与 Kestrel
8.1 线程池机制
1
2
3
4
5
6
7
8
9
10
11
12
13
| .NET 线程池(ThreadPool):
- 托管所有工作线程
- 按需增长(IO 密集型增长快)
- 复用线程(避免频繁创建销毁)
线程池的"饥饿"问题:
- 所有线程都被阻塞(同步 I/O)
- 新任务排队等待
- 响应时间飙升
这就是为什么强调异步:
异步释放线程 → 线程池可用线程多 → 抗并发
同步阻塞线程 → 线程池耗尽 → 系统卡死
|
1
2
3
4
5
6
7
| // 配置线程池(.NET 中通常用环境变量或 ThreadPool.SetMinThreads)
// 设置最小线程数,避免冷启动时线程增长慢
ThreadPool.SetMinThreads(workerThreads: 100, completionPortThreads: 100);
// 推荐:在 Program.cs 早期设置
// 或用环境变量
// DOTNET_ThreadPool_MinThreads=100
|
8.2 Kestrel(ASP.NET Core 服务器)
Kestrel 是 ASP.NET Core 的内置 Web 服务器,性能世界级。
1
2
3
4
5
6
7
8
9
10
11
| Kestrel 为什么快:
✓ 基于 libuv / IOCP(异步 I/O 完成端口)
✓ 完全异步架构(async/await 全链路)
✓ Pipelines 处理网络数据
✓ 内存池(ArrayPool)减少分配
✓ HTTP/2、HTTP/3 支持
性能参考(官方基准):
简单 JSON 接口:单机 10万+ RPS
Plaintext(TechEmpower):单机百万级 RPS
位居 TechEmpower 前列(和 Rust/C++ 一个梯队)
|
1
2
3
4
5
6
7
8
9
10
11
| // Program.cs 配置 Kestrel
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxConcurrentConnections = 10000; // 最大连接数
options.Limits.MaxConcurrentUpgradedConnections = 10000; // WebSocket
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10MB
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
});
|
九、缓存
缓存是高并发的第一道防线——能用缓存的绝不打数据库。
9.1 多级缓存
1
2
3
4
5
6
7
8
| 浏览器缓存 — 客户端,静态资源
CDN — 边缘节点,静态 + 动态加速
Nginx 缓存 — 反向代理层,减少打到应用的请求
内存缓存 — 进程内(IMemoryCache),最快
分布式缓存 — Redis/Memcached,跨实例共享
数据库缓存 — MySQL 查询缓存(已废弃)、缓冲池
层级越靠前,速度越快,容量越小
|
9.2 IMemoryCache(进程内缓存)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 注册
builder.Services.AddMemoryCache();
// 使用
public class UserService
{
private readonly IMemoryCache _cache;
public UserService(IMemoryCache cache) => _cache = cache;
public async Task<User> GetUserAsync(int id)
{
// 缓存键
var key = $"user:{id}";
// GetOrCreateAsync:不存在则加载并缓存
return await _cache.GetOrCreateAsync(key, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30); // 30分钟过期
entry.SlidingExpiration = TimeSpan.FromMinutes(10); // 10分钟无访问过期
return await _db.Users.FindAsync(id);
});
}
}
|
9.3 IDistributedCache + Redis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // 注册 Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
options.InstanceName = "myapp:";
});
// 使用(和 IMemoryCache 接口类似)
public class CacheService
{
private readonly IDistributedCache _cache;
public async Task<string> GetAsync(string key)
{
return await _cache.GetStringAsync(key);
}
public async Task SetAsync(string key, string value)
{
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
};
await _cache.SetStringAsync(key, value, options);
}
}
|
1
2
3
4
5
| IMemoryCache vs IDistributedCache:
IMemoryCache — 进程内,最快,但多实例不共享,重启丢失
IDistributedCache — Redis 等,跨实例共享,持久,但网络开销
实战:本地缓存做一级(抗热点),Redis 做二级(一致性)
|
十、限流与熔断
高并发系统必须保护自己——流量超载时主动拒绝,而不是被拖垮。
10.1 限流(.NET 7+ 内置)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| // .NET 7+ 内置限流中间件
builder.Services.AddRateLimiter(options =>
{
// 全局并发限制
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
httpContext => RateLimitPartition.GetConcurrencyLimiter(
partitionKey: "global",
factory: _ => new ConcurrencyLimiterOptions
{
PermitLimit = 1000, // 全局最多 1000 并发
QueueLimit = 100
}));
// 按 IP 限流
options.AddPolicy("per-ip", httpContext =>
RateLimitPartition.GetTokenBucketLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress!.ToString(),
factory: _ => new TokenBucketRateLimiterOptions
{
TokenLimit = 100,
TokensPerPeriod = 100,
ReplenishmentPeriod = TimeSpan.FromSeconds(1)
}));
options.OnRejected = async (context, ct) =>
{
context.HttpContext.Response.StatusCode = 429;
await context.HttpContext.Response.WriteAsync("Too Many Requests", ct);
};
});
var app = builder.Build();
app.UseRateLimiter();
|
1
2
3
4
5
| 限流算法:
并发限流(ConcurrencyLimiter) — 限制同时在处理的请求数
令牌桶(TokenBucket) — 允许突发,平均速率限制
固定窗口(FixedWindow) — 每个时间窗口固定配额
滑动窗口(SlidingWindow) — 更平滑的窗口算法
|
10.2 熔断、重试、超时(Polly)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 安装
// dotnet add package Microsoft.Extensions.Http.Polly
// dotnet add package Polly
// HttpClient 熔断 + 重试
builder.Services.AddHttpClient("api")
.AddTransientHttpErrorPolicy(policyBuilder =>
policyBuilder.WaitAndRetryAsync(3, attempt =>
TimeSpan.FromSeconds(Math.Pow(2, attempt)))) // 重试 3 次,指数退避
.AddTransientHttpErrorPolicy(policyBuilder =>
policyBuilder.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5, // 连续失败 5 次
durationOfBreak: TimeSpan.FromSeconds(30))); // 熔断 30 秒
// 超时
.AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)));
|
1
2
3
4
5
6
| 熔断器三种状态:
Closed(关闭) — 正常请求
Open(打开) — 失败率达阈值,直接拒绝(快速失败)
Half-Open — 试探性放行几个请求,成功则恢复
作用:下游服务挂了,快速失败,不把整个链路拖死
|
十一、数据库层面
数据库往往是高并发系统的瓶颈所在。
11.1 连接池
1
2
3
4
5
6
7
8
| // ADO.NET / EF Core 默认开启连接池
// 连接字符串配置
"Server=...;Database=...;Pooling=true;Max Pool Size=100;Min Pool Size=10;"
// 注意:
// 连接用完必须 Dispose(using 或 await using)
// 否则连接泄漏,连接池耗尽
await using var conn = new SqlConnection(connStr); // 自动归还连接池
|
11.2 异步 EF Core
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 全异步,不阻塞线程
var users = await _db.Users
.Where(u => u.IsActive)
.ToListAsync(); // 异步查询
// 注意 N+1 问题
// 错误(N+1 查询)
foreach (var user in await _db.Users.ToListAsync())
{
var orders = await _db.Orders.Where(o => o.UserId == user.Id).ToListAsync();
// 100 个用户 = 101 次查询
}
// 正确(Include / Join)
var users = await _db.Users
.Include(u => u.Orders)
.ToListAsync(); // 1 次查询
|
11.3 Dapper(高性能 ORM)
1
2
3
4
5
6
7
| // Dapper 比 EF Core 快 5-10 倍(轻量,接近原生 ADO.NET)
// 适合:性能敏感的查询、复杂 SQL
using var conn = new SqlConnection(connStr);
var users = await conn.QueryAsync<User>(
"SELECT * FROM Users WHERE IsActive = @isActive",
new { isActive = true });
|
1
2
3
4
5
| EF Core vs Dapper:
EF Core — 开发效率高,功能全,性能中等
Dapper — 性能极致,SQL 自己写
实战:核心 CRUD 用 EF Core,热点查询用 Dapper
|
11.4 数据库层面优化
1
2
3
4
5
6
| ✓ 索引优化(避免全表扫描)
✓ 读写分离(主写从读)
✓ 分库分表(数据量大时)
✓ 慢查询日志 + EXPLAIN
✓ 批量操作(减少往返)
✓ 连接池合理配置
|
十二、监控与诊断
高并发系统必须有可观测性,否则出问题两眼一抹黑。
12.1 性能计数器(dotnet-counters)
1
2
3
4
5
6
7
8
9
10
11
12
| # 实时监控 .NET 应用指标
dotnet-counters monitor -p 12345 \
System.Runtime \
Microsoft.AspNetCore.Hosting
# 关注指标:
# cpu-usage CPU 使用率
# gc-heap-size GC 堆大小
# gen-0/1/2-gc-count GC 次数(频繁 GC = 分配过多)
# time-in-gc GC 占用时间
# threadpool-queue-length 线程池队列(长了 = 线程不足/阻塞)
# working-set 内存占用
|
12.2 火焰图(dotnet-trace)
1
2
3
| # 录制性能数据
dotnet-trace collect -p 12345 --format Speedscope -d 30
# 用 https://speedscope.app 打开,看 CPU 时间花在哪
|
12.3 链路追踪(OpenTelemetry)
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 安装
// dotnet add package OpenTelemetry.Extensions.Hosting
// dotnet add package OpenTelemetry.Instrumentation.AspNetCore
// dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing.AddAspNetCoreInstrumentation() // HTTP 请求
.AddHttpClientInstrumentation() // 外部调用
.AddEntityFrameworkCoreInstrumentation() // 数据库
.AddOtlpExporter(); // 导出到 Jaeger/Tempo
});
|
1
2
3
4
5
6
7
| 可观测性三支柱:
Metrics(指标) — CPU、QPS、错误率(聚合数据)
Tracing(追踪) — 一个请求经过的所有服务(链路)
Logging(日志) — 具体的事件记录
工具:OpenTelemetry(标准)+ Prometheus + Grafana + Jaeger
或商业:Application Insights、Datadog
|
十三、高并发架构总结
把前面所有技术组合起来,一个典型的高并发 .NET 系统架构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| 客户端
│
┌─────▼─────┐
│ CDN │ 静态资源缓存
└─────┬─────┘
│
┌─────▼─────┐
│ Nginx │ 负载均衡 + 缓存 + 限流 + HTTPS
│ 集群 │
└─────┬─────┘
│
┌───────────┼───────────┐
│ │ │
┌─────▼─────┐ ┌──▼───┐ ┌─────▼─────┐
│ Kestrel │ │Kestrel│ │ Kestrel │ ASP.NET Core 集群
│ App #1 │ │App #2 │ │ App #3 │ (全异步、Channel、Span)
└─────┬─────┘ └───┬───┘ └─────┬─────┘
│ │ │
└─────┬─────┘───────────┘
│
┌───────────┼───────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Redis │ │ 消息队列 │ │ 数据库 │
│ 缓存 │ │(削峰) │ │(读写分离)│
└─────────┘ └─────────┘ └─────────┘
|
1
2
3
4
5
6
7
8
| 每一层的高并发手段:
CDN/Nginx — 缓存、负载均衡、限流(前文 Nginx 系列讲过)
Kestrel — 异步架构、Pipelines、内存池
应用层 — async/await、Channel、并发集合、对象池
缓存层 — IMemoryCache + Redis 多级缓存
消息队列 — 削峰填谷,异步解耦
数据库 — 连接池、读写分离、分库分表、索引
全链路 — 限流、熔断、重试(Polly)+ 监控(OpenTelemetry)
|
核心原则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| 1. 异步优先
I/O 用 async/await,绝不阻塞线程
2. 减少分配
Span、ArrayPool、对象池,降低 GC 压力
3. 无锁优先
Channel、并发集合、Interlocked,避免锁竞争
4. 缓存为王
能缓存的绝不查数据库,多级缓存
5. 保护自己
限流、熔断、降级,超载时主动拒绝
6. 水平扩展
无状态设计,随时加机器
7. 可观测
指标、追踪、日志,问题可定位
|
十四、小结
本文系统梳理了 .NET 高并发编程的全景:
- 基础概念:高并发的指标、挑战,并发/并行/异步的区别
- 异步编程:async/await 状态机、Task/ValueTask、CancellationToken、IAsyncEnumerable
- 同步原语:lock、SemaphoreSlim、Interlocked、ReaderWriterLockSlim
- 并发集合:ConcurrentDictionary、Channel(明星)、不可变集合
- 并行计算:TPL、Parallel、PLINQ,区分 CPU 并行和 I/O 并发
- 高性能内存:Span/Memory、Pipelines、ArrayPool/ObjectPool
- 服务器:线程池机制、Kestrel 为什么快
- 缓存:IMemoryCache、Redis、多级缓存
- 保护机制:.NET 限流、Polly 熔断重试
- 数据库:连接池、异步 EF Core、Dapper、读写分离
- 可观测性:dotnet-counters、dotnet-trace、OpenTelemetry
- 整体架构:各层高并发手段 + 核心原则
.NET 在高并发领域的能力是被低估的——async/await 模型、Channel、Span、Kestrel 都是顶级实现。掌握这套全景,应对绝大多数高并发场景绰绰有余。
下一篇将深入 .NET 性能优化与 profiling,讲解如何用火焰图定位性能瓶颈。