C# 多线程(四):异步调用容易踩的坑
2023 年 11 月 27 日 · 1 分钟读完
目录
坑一:for 循环里异步调用,参数不对
项目里在 for 循环中用 Task.Run 调用带参数的函数,结果参数全乱了:
static void Main(string[] args)
{
for (int i = 0; i < 100; i++)
{
Task.Run(() => WriteSth(i));
}
Console.ReadKey();
}
static void WriteSth(int index) => Console.WriteLine(index);

本来想打印 0 ~ 99,结果打出来一大堆重复的数,还有 100。
原因:lambda 捕获的是变量 i 本身,不是它当时的值。Task.Run 只是把任务排进线程池,真正执行要等一会儿,而 for 循环不等,i 飞快地往上加。等任务真正运行、去读 i 的时候,它早就变了,循环结束时它是 100。
解决:在循环体里拷贝一份,每次循环都是一个新变量:
for (int i = 0; i < 100; i++)
{
int index = i;
Task.Run(() => WriteSth(index));
}

0 ~ 99 都打出来了,但顺序是乱的:Task.Run 不保证执行顺序。需要顺序就别并行,或者把结果按索引放进数组,最后再统一处理。
foreach从 C# 5 起每次循环都是新变量,没有这个问题;for循环至今仍然共用一个变量。
延伸:另外几个常见的坑
坑二:界面线程上 .Result 或 .Wait() 死锁
private void button1_Click(object sender, EventArgs e)
{
string s = GetDataAsync().Result; // 界面卡死,永远不返回
}
private async Task<string> GetDataAsync()
{
await Task.Delay(1000);
return "ok";
}
await 完成后,默认要回到原来的线程(界面线程)继续执行;而界面线程正阻塞在 .Result 上等它,双方互相等待。解决:从头到尾都用 async,把按钮事件改成 async void,写 string s = await GetDataAsync();。写类库时可以在 await 后面加 .ConfigureAwait(false),不回原线程。
坑三:async void 吞掉异常、无法等待
除了事件处理器,不要写 async void。调用方拿不到 Task,既不能 await 它,也捕获不到里面的异常,未处理的异常会直接让进程崩溃。
坑四:忘了 await
SaveAsync(); // 少了 await:没等保存完就继续往下走,异常也丢了
await SaveAsync(); // 正确
编译器会给出 CS4014 警告,建议在项目里把它设为错误。
坑五:async 方法里用 Thread.Sleep
Thread.Sleep 会阻塞当前线程,在 async 方法里应该用 await Task.Delay(...),等待期间线程可以去干别的。
坑六:多个任务同时改一个集合
List<T> 不是线程安全的,多个 Task 同时 Add 可能丢数据甚至抛异常。用 ConcurrentBag<T>、ConcurrentQueue<T>,或者加 lock。
原文首发于 blog.csdn.net