【问题标题】:thread unsafe async in List<>.ForEach()List<>.ForEach() 中的线程不安全异步
【发布时间】:2020-08-06 20:08:33
【问题描述】:

为什么在.ForEach() 中执行await 是线程不安全的?

在一个 .Net Core 3.1 项目中,我从一些 WebApi 中选择符合条件的用户列表,然后删除它们,最后我再次询问用户列表。当然,这一次我希望列表是空的。

错误代码

    var existing = (await client.GetByMatchAsync(new SearchParameters() {..})).ToList();

    existing.ForEach(async x => await client.DeleteByIdAsync(x.Id));

    var ensure = (await client.GetByMatchAsync(new SearchParameters() {..})).ToList();
    ensure.Count.Should().Be(0);  <-- ERROR WAS 1!

我发现当我在var ensure 之前插入Thread.Sleep(50) 时,代码有效。这清楚地向我表明存在我不理解的线程问题。

工作代码(使用延迟)

            var existing = (await client.GetByMatchAsync(new SearchParameters() {..})).ToList();

            existing.ForEach(async x => await client.DeleteByIdAsync(x.Id));
            Thread.Sleep(50);

            var ensure = (await client.GetByMatchAsync(new SearchParameters() {..})).ToList();
            ensure.Count.Should().Be(0);

替代工作代码(使用foreach)

            var existing = (await client.GetByMatchAsync(new SearchParameters() {..})).ToList();

            foreach (var x in existing)
            {
                await client.DeleteByIdAsync(x.Id);
            }

            var ensure = (await client.GetByMatchAsync(new SearchParameters() {..})).ToList();
            ensure.Count.Should().Be(0);

反编译.ForEach() 看不到线程问题出现在哪里

    public void ForEach(Action<T> action)
    {
        if (action == null)
        {
            ThrowHelper.ThrowArgumentNullException(ExceptionArgument.action);
        }

        int version = _version;

        for (int i = 0; i < _size; i++)
        {
            if (version != _version)
            {
                break;
            }
            action!(_items[i]);
        }

        if (version != _version)
            ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
    }

【问题讨论】:

  • .ForEach() 只是采取行动,而不是 Func,因此您永远不会等待它。
  • existing.ForEach 启动一堆任务,而不是等待它们完成,所以它们都在同时运行。 foreach {await} 在开始下一个任务之前等待上一个任务。
  • 在 ForEach 反编译中,action!(_items[i]);是一个奇怪的说法 感叹号是什么?
  • @CRice 问 ReSharper ! 是什么意思 ;)
  • 这个问题被描述为#5 here

标签: c# linq async-await


【解决方案1】:

List 的 ForEach 只接受 Action&lt;T&gt;,因此每次迭代都不会等待您提供的操作,并且可以在调用完成之前继续执行。

否则签名可能会过载

Task ForEach(Func<T, Task> func)

我认为奇怪的是以下行编译,但您不能将 Func&lt;T, Task&gt; 分配给 Action&lt;T&gt;

Action<string> a = async (value) => await Task.CompletedTask;

您可以以这种方式调用 ForEach 的事实具有误导性。

existing.ForEach(async x => await client.DeleteByIdAsync(x.Id));

【讨论】:

  • 既然我不确定,我只想问:你是否仍然对async (value) =&gt; await Task.CompletedTask 可以同时分配给Fun&lt;string, Task&gt;Action&lt;string&gt; 感到困惑?
  • @Ackdari 我看到它可以像这样,但是如果在不同时间进行,您不能将一个实例分配给另一个实例 - 没有隐式转换。
猜你喜欢
  • 1970-01-01
  • 2021-11-06
  • 2018-11-16
  • 2011-04-06
  • 2020-12-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多