Day 3 - 并发控制
建议用时:260-320 分钟
你将学会什么
- 顺序执行、并发执行、并行执行有什么区别
Task.WhenAll为什么能等待多个任务全部完成Task.WhenAny为什么能拿到最快完成的任务- 为什么并发不是越多越好
SemaphoreSlim如何限制同时运行的任务数量- 并发任务里发生异常时应该怎么处理
并发控制不是为了炫技,而是为了让多个任务一起推进,同时不把文件、网络、数据库或接口压垮。真正的重点是“控制数量”。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
今天只抓住 3 件事
- 并发是多个任务一起推进,不等于无限制同时执行。
Task.WhenAll负责等待一组任务结束。SemaphoreSlim负责限制同时进入核心处理区的任务数量。
学习衔接
上一页学习的是“异步 IO”,今天继续学习“并发控制”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。并发控制的难点不是语法,而是理解“同时做多少才合适”。
1. 顺序执行是什么
顺序执行就是一个做完,再做下一个。
例如 4 个任务,每个 1 秒:
任务1 -> 任务2 -> 任务3 -> 任务4总耗时大约 4 秒。
优点是简单、稳定。
缺点是有些等待时间被浪费了。
2. 并发执行是什么
并发执行是多个任务在同一段时间里一起推进。
例如 4 个异步等待任务一起开始:
任务1 开始
任务2 开始
任务3 开始
任务4 开始
一起等待
一起完成总耗时可能接近 1 秒。
注意:并发不等于一定在多个 CPU 核心上同时计算。对于 IO 等待来说,并发更多是让多个等待重叠。
3. 并行执行是什么
并行更强调“真的同时执行”,通常和多个 CPU 核心有关。
本页先不深入并行计算,只要分清:
| 概念 | 重点 |
|---|---|
| 异步 | 等待期间不堵住 |
| 并发 | 多个任务在同一段时间推进 |
| 并行 | 多个任务真正同时计算 |
文件、网络、数据库等待,优先考虑异步并发。
大量 CPU 计算,后面再考虑并行计算。
4. Task.WhenAll 是什么
Task.WhenAll 表示:
等一组任务全部完成。常见场景:
- 同时读取多个文件。
- 同时请求多个接口。
- 同时处理多个订单。
- 等所有任务完成后统一汇总结果。
如果任务有返回值:
Task<string>[]await Task.WhenAll(...) 后可以得到:
string[]5. Task.WhenAny 是什么
Task.WhenAny 表示:
等一组任务里最快完成的那个。常见场景:
- 多个来源谁先返回用谁。
- 一个任务和一个超时任务竞争。
- 先展示最快结果,慢的后面再处理。
注意:WhenAny 只告诉你哪个任务先完成,不代表其他任务自动停止。
6. 为什么并发不是越多越好
如果有 1000 个订单,同时开 1000 个任务,看起来很快,但可能会出问题:
| 资源 | 可能被压垮 |
|---|---|
| 文件 | 同时打开太多文件 |
| 网络 | 同时请求太多接口 |
| 数据库 | 连接池耗尽 |
| 第三方接口 | 被限流或封禁 |
| 本机内存 | 任务太多占用过高 |
所以并发要控制数量。
7. SemaphoreSlim 是什么
SemaphoreSlim 可以理解成“有限名额的门”。
例如:
SemaphoreSlim(2)表示同一时间最多 2 个任务通过。
核心写法:
await semaphore.WaitAsync()
try
{
做真正任务
}
finally
{
semaphore.Release()
}WaitAsync() 拿名额。
Release() 还名额。
8. 为什么 Release 要放在 finally
如果处理任务中间发生异常,但没有释放名额,后面的任务可能永远等不到名额。
所以固定写法是:
await semaphore.WaitAsync()
try
{
...
}
finally
{
semaphore.Release()
}这不是形式主义,而是防止程序卡死。
9. 并发任务里的异常
Task.WhenAll 中只要有任务失败,await Task.WhenAll(...) 就会抛异常。
有两种处理思路:
| 思路 | 适合场景 |
|---|---|
外层统一 try/catch | 任意一个失败就认为整体失败 |
| 每个任务内部自己捕获 | 希望成功和失败分开统计 |
批量处理更常用第二种,因为一个订单失败,不一定要让所有订单都失败。
第二部分:把知识组合成完整例子
前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要能做到:有很多任务,但一次最多只让几个进入核心处理区。
先看效果:一次最多处理 2 个订单
using System.Diagnostics;
int[] orderIds = { 1, 2, 3, 4, 5, 6 };
using SemaphoreSlim gate = new SemaphoreSlim(2);
Stopwatch stopwatch = Stopwatch.StartNew();
Task[] tasks = orderIds
async Task ProcessOrderWithLimitAsync(int orderId, SemaphoreSlim semaphore)
{
await semaphore.WaitAsync();
try
{
Console.WriteLine($"开始处理订单 {orderId}");
await Task.Delay(1000);
Console.WriteLine($"完成处理订单 {orderId}");
}
finally
{
semaphore.Release();
}
}
.Select(id => ProcessOrderWithLimitAsync(id, gate))
.ToArray();
await Task.WhenAll(tasks);
stopwatch.Stop();
Console.WriteLine($"全部完成,耗时约 {stopwatch.ElapsedMilliseconds} 毫秒");
这里最重要的是三件事:
Task.WhenAll(tasks)等所有任务完成。SemaphoreSlim(2)表示最多放 2 个任务进去。Release()放在finally,保证出错时也会释放名额。
第三部分:跟着敲代码
从这里开始动手。每个例子都是完整代码,可以直接放进 Program.cs 运行。
动手前先做这 3 件事
- 打开一个控制台项目。
- 每次只保留一个例子的代码,运行通过后再换下一个。
- 每个例子运行后,改任务数量、等待时间或并发上限,再运行观察结果。
例子 1:顺序执行
using System.Diagnostics;
int[] ids = { 1, 2, 3, 4 };
Stopwatch stopwatch = Stopwatch.StartNew();
foreach (int id in ids)
{
async Task ProcessAsync(int id)
{
Console.WriteLine($"开始任务 {id}");
await Task.Delay(500);
Console.WriteLine($"完成任务 {id}");
}
await ProcessAsync(id);
}
stopwatch.Stop();
Console.WriteLine($"顺序执行耗时约: {stopwatch.ElapsedMilliseconds} 毫秒");
这个版本一次只处理一个任务,4 个任务大约需要 2 秒。
例子 2:并发执行并等待全部完成
using System.Diagnostics;
int[] ids = { 1, 2, 3, 4 };
Stopwatch stopwatch = Stopwatch.StartNew();
Task[] tasks = ids
async Task ProcessAsync(int id)
{
Console.WriteLine($"开始任务 {id}");
await Task.Delay(500);
Console.WriteLine($"完成任务 {id}");
}
.Select(id => ProcessAsync(id))
.ToArray();
await Task.WhenAll(tasks);
stopwatch.Stop();
Console.WriteLine($"并发执行耗时约: {stopwatch.ElapsedMilliseconds} 毫秒");
这个版本 4 个任务几乎同时开始,总耗时接近 500 毫秒。
例子 3:Task.WhenAll 返回结果
int[] ids = { 1, 2, 3 };
Task<string>[] tasks = ids
async Task<string> LoadOrderAsync(int id)
{
await Task.Delay(300);
return $"订单 SO-{id:000}";
}
.Select(id => LoadOrderAsync(id))
.ToArray();
string[] results = await Task.WhenAll(tasks);
foreach (string result in results)
{
Console.WriteLine(result);
}
如果每个任务返回 string,WhenAll 完成后得到 string[]。
例子 4:Task.WhenAny 等最快任务
async Task<string> LoadFromCacheAsync()
{
await Task.Delay(300);
return "缓存数据";
}
async Task<string> LoadFromApiAsync()
{
await Task.Delay(1000);
return "接口数据";
}
Task<string> cacheTask = LoadFromCacheAsync();
Task<string> apiTask = LoadFromApiAsync();
Task<string> completedTask = await Task.WhenAny(cacheTask, apiTask);
string result = await completedTask;
Console.WriteLine($"最快结果: {result}");
WhenAny 拿到的是最快完成的那个任务。要得到结果,还需要再 await completedTask。
例子 5:无限制并发
int[] ids = Enumerable.Range(1, 10).ToArray();
Task[] tasks = ids
async Task ProcessAsync(int id)
{
Console.WriteLine($"开始任务 {id}");
await Task.Delay(500);
Console.WriteLine($"完成任务 {id}");
}
.Select(id => ProcessAsync(id))
.ToArray();
await Task.WhenAll(tasks);
10 个任务会几乎同时开始。任务少时没问题,任务很多时就需要限制数量。
例子 6:使用 SemaphoreSlim 限制并发
int[] ids = Enumerable.Range(1, 10).ToArray();
using SemaphoreSlim semaphore = new SemaphoreSlim(3);
Task[] tasks = ids
async Task ProcessWithLimitAsync(int id, SemaphoreSlim gate)
{
await gate.WaitAsync();
try
{
Console.WriteLine($"进入处理区: {id}");
await Task.Delay(500);
Console.WriteLine($"离开处理区: {id}");
}
finally
{
gate.Release();
}
}
.Select(id => ProcessWithLimitAsync(id, semaphore))
.ToArray();
await Task.WhenAll(tasks);
SemaphoreSlim(3) 表示同一时间最多 3 个任务进入处理区。
例子 7:异常时也要释放名额
int[] ids = { 1, 2, 3, 4, 5 };
using SemaphoreSlim semaphore = new SemaphoreSlim(2);
Task[] tasks = ids
async Task ProcessWithLimitAsync(int id, SemaphoreSlim gate)
{
await gate.WaitAsync();
try
{
Console.WriteLine($"开始任务 {id}");
await Task.Delay(300);
if (id == 3)
{
throw new InvalidOperationException("任务 3 模拟失败");
}
Console.WriteLine($"完成任务 {id}");
}
finally
{
gate.Release();
Console.WriteLine($"释放名额: {id}");
}
}
.Select(id => ProcessWithLimitAsync(id, semaphore))
.ToArray();
try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
Console.WriteLine($"批量处理失败: {ex.Message}");
}
即使任务 3 失败,finally 仍然会释放名额,后面的任务才有机会继续。
例子 8:每个任务内部处理自己的失败
int[] ids = { 1, 2, 3, 4, 5 };
Task<ProcessResult>[] tasks = ids
async Task<ProcessResult> ProcessSafeAsync(int id)
{
try
{
await Task.Delay(300);
if (id == 3)
{
throw new InvalidOperationException("模拟失败");
}
return new ProcessResult(id, true, "完成");
}
catch (Exception ex)
{
return new ProcessResult(id, false, ex.Message);
}
}
.Select(id => ProcessSafeAsync(id))
.ToArray();
ProcessResult[] results = await Task.WhenAll(tasks);
foreach (ProcessResult result in results)
{
Console.WriteLine($"{result.Id} | 成功: {result.Success} | {result.Message}");
}
class ProcessResult
{
public ProcessResult(int id, bool success, string message)
{
Id = id;
Success = success;
Message = message;
}
public int Id { get; }
public bool Success { get; }
public string Message { get; }
}批量处理时,这种写法很常见:每个任务返回自己的成功或失败结果,最后统一汇总。
例子 9:限制并发并收集结果
int[] ids = Enumerable.Range(1, 8).ToArray();
using SemaphoreSlim semaphore = new SemaphoreSlim(2);
Task<ProcessResult>[] tasks = ids
async Task<ProcessResult> ProcessWithLimitAsync(int id, SemaphoreSlim gate)
{
await gate.WaitAsync();
try
{
await Task.Delay(300);
if (id % 4 == 0)
{
return new ProcessResult(id, false, "模拟业务失败");
}
return new ProcessResult(id, true, "处理完成");
}
finally
{
gate.Release();
}
}
.Select(id => ProcessWithLimitAsync(id, semaphore))
.ToArray();
ProcessResult[] results = await Task.WhenAll(tasks);
int successCount = results.Count(result => result.Success);
int failedCount = results.Length - successCount;
Console.WriteLine($"成功: {successCount}");
Console.WriteLine($"失败: {failedCount}");
foreach (ProcessResult result in results)
{
Console.WriteLine($"{result.Id} | {result.Message}");
}
class ProcessResult
{
public ProcessResult(int id, bool success, string message)
{
Id = id;
Success = success;
Message = message;
}
public int Id { get; }
public bool Success { get; }
public string Message { get; }
}这个例子把本页关键内容合在一起:批量任务、限制并发、收集结果。
例子 10:异步读取多个文件并限制并发
Directory.CreateDirectory("data");
await File.WriteAllTextAsync("data/a.txt", "A");
await File.WriteAllTextAsync("data/b.txt", "B");
await File.WriteAllTextAsync("data/c.txt", "C");
await File.WriteAllTextAsync("data/d.txt", "D");
string[] paths = Directory.GetFiles("data", "*.txt");
using SemaphoreSlim semaphore = new SemaphoreSlim(2);
Task<FileResult>[] tasks = paths
async Task<FileResult> ReadFileWithLimitAsync(string path, SemaphoreSlim gate)
{
await gate.WaitAsync();
try
{
string content = await File.ReadAllTextAsync(path);
return new FileResult(path, content);
}
finally
{
gate.Release();
}
}
.Select(path => ReadFileWithLimitAsync(path, semaphore))
.ToArray();
FileResult[] results = await Task.WhenAll(tasks);
foreach (FileResult result in results)
{
Console.WriteLine($"{result.Path} -> {result.Content}");
}
class FileResult
{
public FileResult(string path, string content)
{
Path = path;
Content = content;
}
public string Path { get; }
public string Content { get; }
}文件 IO 也可以批量并发,但同样要注意不要同时打开太多文件。
并发控制常用写法速查
| 需求 | 写法 | 作用 |
|---|---|---|
| 创建多个任务 | items.Select(item => WorkAsync(item)).ToArray() | 把多条数据变成一组任务 |
| 等全部完成 | await Task.WhenAll(tasks) | 所有任务结束后继续 |
| 等最快完成 | await Task.WhenAny(tasks) | 先拿到最快结果 |
| 限制并发数 | new SemaphoreSlim(2) | 同时最多 2 个任务进入 |
| 拿名额 | await gate.WaitAsync() | 进入受限区域 |
| 还名额 | gate.Release() | 允许后续任务进入 |
| 保证归还 | finally { gate.Release(); } | 出错也不占住名额 |
| 统计耗时 | Stopwatch.StartNew() | 观察并发效果 |
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 开很多任务但不限制数量 | 文件、网络、数据库可能被压垮 | 用 SemaphoreSlim 限制并发 |
Release() 不放 finally | 任务出错后名额不会归还 | 固定写 try/finally |
只创建任务不 await | 程序可能提前结束 | 用 await Task.WhenAll(tasks) |
| 把并发当成并行计算 | IO 等待和 CPU 计算不是一回事 | 文件、网络优先异步并发 |
| 任务失败后不知道哪一个失败 | 没有记录任务输入和错误 | 返回结果对象,包含 id、成功状态、错误消息 |
小白重复敲写训练
并发先从两个短任务开始,先观察顺序,再增加数量。
训练 1:两个任务一起等待
async Task WorkAsync(string name, int delay)
{
await Task.Delay(delay);
Console.WriteLine(name);
}
Task first = WorkAsync("A", 1000);
Task second = WorkAsync("B", 500);
await Task.WhenAll(first, second);
Console.WriteLine("全部完成");
先预测 A、B 谁先输出。
训练 2:并发得到多个结果
async Task<int> GetValueAsync(int value)
{
await Task.Delay(300);
return value;
}
Task<int> first = GetValueAsync(10);
Task<int> second = GetValueAsync(20);
int[] values = await Task.WhenAll(first, second);
Console.WriteLine(values.Sum());
改动任务:加入第三个任务,返回 30。
训练 3:限制同时执行数量
var gate = new SemaphoreSlim(2);
async Task WorkAsync(int id)
{
await gate.WaitAsync();
try
{
Console.WriteLine($"{id} 开始");
await Task.Delay(500);
}
finally
{
gate.Release();
}
}
await Task.WhenAll(Enumerable.Range(1, 5).Select(WorkAsync));观察同一时间最多有几个任务开始。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:并发控制。今天至少要掌握的 3 个点是:
1. 顺序执行、并发执行、并行执行有什么区别
2. `Task.WhenAll` 为什么能等待多个任务全部完成
3. `Task.WhenAny` 为什么能拿到最快完成的任务答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:顺序执行、并发执行、并行执行有什么区别。
上位机专项练习
多台设备可以并发读取,但不能无节制地同时连接。练习 WhenAll、WhenAny 和 SemaphoreSlim。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:并发读取三台设备
class Program
{
static async Task<string> ReadAsync(string name)
{
await Task.Delay(300);
return $"{name}: 25.0";
}
static async Task Main()
{
string[] names = ["PLC-01", "PLC-02", "PLC-03"];
Task<string>[] tasks = names.Select(ReadAsync).ToArray();
string[] results = await Task.WhenAll(tasks);
foreach (string result in results) Console.WriteLine(result);
}
}运行结果或界面效果:
PLC-01: 25.0
PLC-02: 25.0
PLC-03: 25.0改动任务: 增加 PLC-04。
专项例子 2:先显示最快返回的设备
class Program
{
static async Task<string> ReadAsync(string name, int delay)
{
await Task.Delay(delay);
return name;
}
static async Task Main()
{
Task<string>[] tasks = [ReadAsync("PLC-01", 600), ReadAsync("PLC-02", 200)];
Task<string> first = await Task.WhenAny(tasks);
Console.WriteLine($"最先返回: {await first}");
}
}运行结果或界面效果:
最先返回: PLC-02改动任务: 交换两个等待时间,观察结果。
专项例子 3:限制最多两个连接
class Program
{
static readonly SemaphoreSlim Gate = new(2);
static async Task ReadAsync(string name)
{
await Gate.WaitAsync();
try { Console.WriteLine($"开始 {name}"); await Task.Delay(300); }
finally { Gate.Release(); }
}
static async Task Main()
{
await Task.WhenAll([ReadAsync("A"), ReadAsync("B"), ReadAsync("C")]);
}
}运行结果或界面效果:
A、B 先开始,C 等待空位改动任务: 把并发数从 2 改成 1。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。建议先照着敲一遍,再修改并发上限验证效果。
作业 1:并发读取订单
要求:
- 准备 6 个订单编号。
- 写
LoadOrderAsync模拟读取订单。 - 用
Task.WhenAll同时读取。 - 输出所有结果。
完整答案
int[] ids = { 1, 2, 3, 4, 5, 6 };
Task<string>[] tasks = ids
async Task<string> LoadOrderAsync(int id)
{
await Task.Delay(300);
return $"订单 SO-{id:000}";
}
.Select(id => LoadOrderAsync(id))
.ToArray();
string[] orders = await Task.WhenAll(tasks);
foreach (string order in orders)
{
Console.WriteLine(order);
}
作业 2:一次最多处理 2 个任务
要求:
- 准备 8 个任务编号。
- 使用
SemaphoreSlim(2)。 - 一次最多 2 个任务进入处理区。
- 所有任务最终完成。
完整答案
int[] ids = Enumerable.Range(1, 8).ToArray();
using SemaphoreSlim semaphore = new SemaphoreSlim(2);
Task[] tasks = ids
async Task ProcessAsync(int id, SemaphoreSlim gate)
{
await gate.WaitAsync();
try
{
Console.WriteLine($"开始处理: {id}");
await Task.Delay(500);
Console.WriteLine($"处理完成: {id}");
}
finally
{
gate.Release();
}
}
.Select(id => ProcessAsync(id, semaphore))
.ToArray();
await Task.WhenAll(tasks);
作业 3:最快任务
要求:
- 写两个异步读取方法。
- 一个 300 毫秒返回,一个 1000 毫秒返回。
- 使用
Task.WhenAny拿最快结果。
完整答案
async Task<string> LoadLocalAsync()
{
await Task.Delay(300);
return "本地缓存";
}
async Task<string> LoadRemoteAsync()
{
await Task.Delay(1000);
return "远程接口";
}
Task<string> localTask = LoadLocalAsync();
Task<string> remoteTask = LoadRemoteAsync();
Task<string> firstTask = await Task.WhenAny(localTask, remoteTask);
string firstResult = await firstTask;
Console.WriteLine($"最快结果: {firstResult}");
作业 4:限制并发并统计成功失败
要求:
- 准备 10 个任务。
- 一次最多处理 3 个。
- 编号能被 4 整除的任务返回失败。
- 最后统计成功和失败数量。
完整答案
int[] ids = Enumerable.Range(1, 10).ToArray();
using SemaphoreSlim semaphore = new SemaphoreSlim(3);
Task<ProcessResult>[] tasks = ids
async Task<ProcessResult> ProcessAsync(int id, SemaphoreSlim gate)
{
await gate.WaitAsync();
try
{
await Task.Delay(300);
if (id % 4 == 0)
{
return new ProcessResult(id, false, "业务失败");
}
return new ProcessResult(id, true, "处理成功");
}
finally
{
gate.Release();
}
}
.Select(id => ProcessAsync(id, semaphore))
.ToArray();
ProcessResult[] results = await Task.WhenAll(tasks);
int successCount = results.Count(result => result.Success);
int failedCount = results.Length - successCount;
Console.WriteLine($"成功数量: {successCount}");
Console.WriteLine($"失败数量: {failedCount}");
foreach (ProcessResult result in results)
{
Console.WriteLine($"{result.Id} | {result.Message}");
}
class ProcessResult
{
public ProcessResult(int id, bool success, string message)
{
Id = id;
Success = success;
Message = message;
}
public int Id { get; }
public bool Success { get; }
public string Message { get; }
}作业 5:批量读取文件并限制并发
要求:
- 创建 5 个文本文件。
- 一次最多读取 2 个。
- 读取后输出文件名和内容。
完整答案
Directory.CreateDirectory("data");
for (int i = 1; i <= 5; i++)
{
await File.WriteAllTextAsync($"data/{i}.txt", $"文件 {i}");
}
string[] paths = Directory.GetFiles("data", "*.txt");
using SemaphoreSlim semaphore = new SemaphoreSlim(2);
Task<FileResult>[] tasks = paths
async Task<FileResult> ReadFileAsync(string path, SemaphoreSlim gate)
{
await gate.WaitAsync();
try
{
string content = await File.ReadAllTextAsync(path);
return new FileResult(path, content);
}
finally
{
gate.Release();
}
}
.Select(path => ReadFileAsync(path, semaphore))
.ToArray();
FileResult[] results = await Task.WhenAll(tasks);
foreach (FileResult result in results)
{
Console.WriteLine($"{result.Path} -> {result.Content}");
}
class FileResult
{
public FileResult(string path, string content)
{
Path = path;
Content = content;
}
public string Path { get; }
public string Content { get; }
}本页最后要记住
Task.WhenAll等全部任务完成。Task.WhenAny等最快完成的任务。- 并发不是越多越好,要看资源能不能承受。
SemaphoreSlim可以限制同一时间进入处理区的任务数量。WaitAsync拿名额,Release还名额。Release必须放在finally里。- 批量任务最好能统计成功和失败,而不是一个失败就看不清全局。