C# 多线程
`Thread`、`Task`、`async/await`、`lock`、以及并发集合。
Task 与 async/await
async Task<string> FetchAsync(string name, int ms)
{
await Task.Delay(ms);
return $"{name} 完成";
}
var sw = System.Diagnostics.Stopwatch.StartNew();
// 串行
var a = await FetchAsync("A", 100);
var b = await FetchAsync("B", 100);
Console.WriteLine($"{a} {b} 串行 {sw.ElapsedMilliseconds}ms");
sw.Restart();
// 并发
var t1 = FetchAsync("A", 100);
var t2 = FetchAsync("B", 100);
var results = await Task.WhenAll(t1, t2);
Console.WriteLine($"{string.Join(" ", results)} 并发 {sw.ElapsedMilliseconds}ms");A 完成 B 完成 串行 207ms
A 完成 B 完成 并发 105msStart the tasks, then await to get concurrency. await FetchAsync(...) waits immediately so the next never starts — the classic "I thought this was concurrent".
Task 常用 API
// 全部完成
var all = await Task.WhenAll(
Task.FromResult(1),
Task.FromResult(2)
);
Console.WriteLine(string.Join(",", all));
// 任一完成
var first = await Task.WhenAny(
Task.Delay(50).ContinueWith(_ => "快"),
Task.Delay(200).ContinueWith(_ => "慢")
);
Console.WriteLine(first.Result);
// 起一个后台任务
var t = Task.Run(() =>
{
int sum = 0;
for (int i = 0; i < 1000; i++) sum += i;
return sum;
});
Console.WriteLine(await t);
// 超时
using var cts = new CancellationTokenSource(100);
try
{
await Task.Delay(1000, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("超时取消");
}1,2
快
499500
超时取消| API | Meaning |
|---|---|
Task.Run | 在线程池跑 CPU 任务 |
Task.Delay | 异步等待(不阻塞线程) |
Task.WhenAll | 等全部完成 |
Task.WhenAny | 等任一完成 |
CancellationToken | 取消信号 |
Task.FromResult | 包装已有值 |
lock 与共享状态
class Counter
{
private int _n;
private readonly object _gate = new();
public void Inc()
{
lock (_gate)
{
_n++;
}
}
public int Value { get { lock (_gate) return _n; } }
}
var c = new Counter();
await Task.WhenAll(Enumerable.Range(0, 1000).Select(_ => Task.Run(() => c.Inc())));
Console.WriteLine(c.Value);1000lock uses Monitor: one thread at a time. The lock object must be private, a reference type, and not locked elsewhere — lock(this) or lock(typeof(X)) can deadlock.
并发集合
using System.Collections.Concurrent;
var bag = new ConcurrentBag<int>();
var dict = new ConcurrentDictionary<string, int>();
var queue = new ConcurrentQueue<int>();
await Task.WhenAll(Enumerable.Range(0, 100).Select(i => Task.Run(() =>
{
bag.Add(i);
dict.AddOrUpdate("count", 1, (_, old) => old + 1);
queue.Enqueue(i);
})));
Console.WriteLine(bag.Count);
Console.WriteLine(dict["count"]);
Console.WriteLine(queue.Count);100
100
100| Collection | Use |
|---|---|
ConcurrentDictionary | 并发键值缓存 |
ConcurrentQueue | 生产者消费者队列 |
ConcurrentBag | 无序收集 |
BlockingCollection | 有界队列,阻塞式 |
Channel<T> | 现代异步生产者消费者 |
ConcurrentDictionary's AddOrUpdate/GetOrAdd are atomic, but the delegate may run more than once under contention. Keep delegates side-effect free.async 的三条铁律
// 1. 不要 async void(除了事件处理器)——异常无法捕获
// async void Bad() { await Task.Delay(1); throw new Exception(); }
// 2. 不要 .Result / .Wait()——会阻塞线程,可能死锁
// var x = FetchAsync().Result; // 危险
// 3. 一路 async 到底(async all the way)
async Task<int> GetAsync() => await Task.FromResult(1);
async Task RunAsync() => Console.WriteLine(await GetAsync());
await RunAsync();
// 4. 库代码用 ConfigureAwait(false) 避免回到原上下文
async Task LibAsync()
{
await Task.Delay(1).ConfigureAwait(false);
}
await LibAsync();1.Result deadlocks on a UI thread or classic ASP.NET context: the main thread blocks waiting while the await continuation needs that same thread. The classic C# async trap.
选哪个
| Need | Use |
|---|---|
| I/O 等待(HTTP、数据库、文件) | async/await |
| CPU 密集计算 | Task.Run 或 Parallel.For |
| 定时后台任务 | BackgroundService / PeriodicTimer |
| 极细粒度控制线程 | Thread(很少用) |
| 大量数据并行 | Parallel.ForEach / PLINQ |
Task 与 async/await
async Task<string> FetchAsync(string name, int ms)
{
await Task.Delay(ms);
return $"{name} 完成";
}
var sw = System.Diagnostics.Stopwatch.StartNew();
// 串行
var a = await FetchAsync("A", 100);
var b = await FetchAsync("B", 100);
Console.WriteLine($"{a} {b} 串行 {sw.ElapsedMilliseconds}ms");
sw.Restart();
// 并发
var t1 = FetchAsync("A", 100);
var t2 = FetchAsync("B", 100);
var results = await Task.WhenAll(t1, t2);
Console.WriteLine($"{string.Join(" ", results)} 并发 {sw.ElapsedMilliseconds}ms");A 完成 B 完成 串行 207ms
A 完成 B 完成 并发 105ms**先启动任务再 await** 才是并发。await FetchAsync(...) 立刻等它,下一个任务根本没开始——这是最常见的「以为写了并发其实是串行」。
Task 常用 API
// 全部完成
var all = await Task.WhenAll(
Task.FromResult(1),
Task.FromResult(2)
);
Console.WriteLine(string.Join(",", all));
// 任一完成
var first = await Task.WhenAny(
Task.Delay(50).ContinueWith(_ => "快"),
Task.Delay(200).ContinueWith(_ => "慢")
);
Console.WriteLine(first.Result);
// 起一个后台任务
var t = Task.Run(() =>
{
int sum = 0;
for (int i = 0; i < 1000; i++) sum += i;
return sum;
});
Console.WriteLine(await t);
// 超时
using var cts = new CancellationTokenSource(100);
try
{
await Task.Delay(1000, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("超时取消");
}1,2
快
499500
超时取消| API | 含义 |
|---|---|
Task.Run | 在线程池跑 CPU 任务 |
Task.Delay | 异步等待(不阻塞线程) |
Task.WhenAll | 等全部完成 |
Task.WhenAny | 等任一完成 |
CancellationToken | 取消信号 |
Task.FromResult | 包装已有值 |
lock 与共享状态
class Counter
{
private int _n;
private readonly object _gate = new();
public void Inc()
{
lock (_gate)
{
_n++;
}
}
public int Value { get { lock (_gate) return _n; } }
}
var c = new Counter();
await Task.WhenAll(Enumerable.Range(0, 1000).Select(_ => Task.Run(() => c.Inc())));
Console.WriteLine(c.Value);1000lock 用 Monitor 实现,同一时刻只有一个线程能进入。锁对象必须是私有的、引用类型的、不会被外部锁的——lock(this) 或 lock(typeof(X)) 都可能死锁。
并发集合
using System.Collections.Concurrent;
var bag = new ConcurrentBag<int>();
var dict = new ConcurrentDictionary<string, int>();
var queue = new ConcurrentQueue<int>();
await Task.WhenAll(Enumerable.Range(0, 100).Select(i => Task.Run(() =>
{
bag.Add(i);
dict.AddOrUpdate("count", 1, (_, old) => old + 1);
queue.Enqueue(i);
})));
Console.WriteLine(bag.Count);
Console.WriteLine(dict["count"]);
Console.WriteLine(queue.Count);100
100
100| 集合 | 用途 |
|---|---|
ConcurrentDictionary | 并发键值缓存 |
ConcurrentQueue | 生产者消费者队列 |
ConcurrentBag | 无序收集 |
BlockingCollection | 有界队列,阻塞式 |
Channel<T> | 现代异步生产者消费者 |
ConcurrentDictionary 的 AddOrUpdate / GetOrAdd 是原子的,但传给它的委托可能被执行多次(并发时)。所以委托里不要有副作用——只做纯计算。async 的三条铁律
// 1. 不要 async void(除了事件处理器)——异常无法捕获
// async void Bad() { await Task.Delay(1); throw new Exception(); }
// 2. 不要 .Result / .Wait()——会阻塞线程,可能死锁
// var x = FetchAsync().Result; // 危险
// 3. 一路 async 到底(async all the way)
async Task<int> GetAsync() => await Task.FromResult(1);
async Task RunAsync() => Console.WriteLine(await GetAsync());
await RunAsync();
// 4. 库代码用 ConfigureAwait(false) 避免回到原上下文
async Task LibAsync()
{
await Task.Delay(1).ConfigureAwait(false);
}
await LibAsync();1.Result 在 UI 线程或 ASP.NET(旧版)上下文里会死锁:主线程阻塞等结果,而 await 的后续要回到主线程才能完成。这是 C# 异步最经典的坑。
选哪个
| 需求 | 用 |
|---|---|
| I/O 等待(HTTP、数据库、文件) | async/await |
| CPU 密集计算 | Task.Run 或 Parallel.For |
| 定时后台任务 | BackgroundService / PeriodicTimer |
| 极细粒度控制线程 | Thread(很少用) |
| 大量数据并行 | Parallel.ForEach / PLINQ |