Owl*_*Owl 3 c# asynchronous race-condition async-await .net-5
.NET 5.0
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
namespace AsyncTest
{
[TestClass]
public class AsyncTest
{
public async Task AppendNewIntVal(List<int> intVals)
{
await Task.Delay(new Random().Next(15, 45));
intVals.Add(new Random().Next());
}
public async Task AppendNewIntVal(int count, List<int> intVals)
{
var appendNewIntValTasks = new List<Task>();
for (var a = 0; a < count; a++)
{
appendNewIntValTasks.Add(AppendNewIntVal(intVals));
}
await Task.WhenAll(appendNewIntValTasks);
}
[TestMethod]
public async Task TestAsyncIntList()
{
var appendCount = 30;
var intVals = new List<int>();
await AppendNewIntVal(appendCount, intVals);
Assert.AreEqual(appendCount, intVals.Count);
}
}
}
Run Code Online (Sandbox Code Playgroud)
上面的代码编译并运行,但测试失败,输出类似于:
Assert.AreEqual 失败。预期:<30>。实际:<17>。
在上面的示例中,“实际”值为 17,但它在执行之间有所不同。
我知道我对异步编程在 .NET 中的工作方式缺乏一些了解,因为我没有得到预期的输出。
根据我的理解,该AppendNewIntVal方法启动 N 个任务,然后等待它们全部完成。如果他们都完成了,我希望他们每个人都会在列表中附加一个值,但事实并非如此。看起来存在竞争条件,但我认为这是不可能的,因为代码不是多线程的。我错过了什么?
是的,如果您不立即等待每个可等待的,即在这里:
appendNewIntValTasks.Add(AppendNewIntVal(intVals));
Run Code Online (Sandbox Code Playgroud)
这一行在异步术语中等同于 (in thread-based code) Thread.Start,我们现在在内部异步代码周围没有安全性:
intVals.Add(new Random().Next());
Run Code Online (Sandbox Code Playgroud)
当两个流同时调用Add时,它现在可以以相同的并发方式失败。您也应该避免new Random(),因为这不一定是随机的(它是基于许多框架版本的时间,并且最终可能会导致两个流获得相同的种子)。
所以:所示的代码确实很危险。
在明显的安全版本是:
public async Task AppendNewIntVal(int count, List<int> intVals)
{
for (var a = 0; a < count; a++)
{
await AppendNewIntVal(intVals);
}
}
Run Code Online (Sandbox Code Playgroud)
它可能推迟await,但你明确选择加入并发当你这样做,你的代码需要处理它适合防守。