首页 > 解决方案 > 仅使用 async/await 时是否存在竞争条件?

问题描述

.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);
        }
    }
}

上面的代码编译并运行,但测试失败,输出类似于:

Assert.AreEqual 失败。预期:<30>。实际:<17>。

在上面的示例中,“实际”值是 17,但它在执行之间会有所不同。

我知道我对异步编程在 .NET 中的工作方式有一些了解,因为我没有得到预期的输出。

据我了解,该AppendNewIntVal方法启动了 N 个任务,然后等待它们全部完成。如果他们都完成了,我希望他们每个人都会在列表中附加一个值,但事实并非如此。看起来有竞争条件,但我认为这是不可能的,因为代码不是多线程的。我错过了什么?

标签: c#asynchronousasync-awaitrace-condition.net-5

解决方案


是的,如果您不立即等待每个可等待对象,即此处:

appendNewIntValTasks.Add(AppendNewIntVal(intVals));

这一行在异步术语中等同于 (in thread-based code) Thread.Start,我们现在在内部异步代码周围没有安全性:

intVals.Add(new Random().Next());

当两个流同时调用时,它现在可以以相同的并发方式失败Add。您还应该避免new Random(),因为这不一定是随机的(这是基于许多框架版本的时间,并且最终可能导致两个流获得相同的种子)。

所以:显示的代码确实很危险。

显然安全的版本是:

        public async Task AppendNewIntVal(int count, List<int> intVals)
        {
            for (var a = 0; a < count; a++)
            {
                await AppendNewIntVal(intVals);
            }
        }

可以推迟,但是当你这样做时,你明确地选择了并发,并且你的代码需要适当地防御性地处理它await


推荐阅读