【问题标题】:Why async functions are called twice?为什么异步函数被调用两次?
【发布时间】:2016-06-18 16:47:18
【问题描述】:

我正在使用线程定时器来做一些周期性的工作:

private static async void TimerCallback(object state)
{
        if (Interlocked.CompareExchange(ref currentlyRunningTasksCount, 1, 0) != 0)
        {
            return;
        }

        var tasksRead = Enumerable.Range(3, 35).Select(i => ReadSensorsAsync(i));
        await Task.WhenAll(tasksRead);
        var tasksRecord = tasksRead.Where(x => x.Result != null).Select(x => RecordReadingAsync(x.Result));
        await Task.WhenAll(tasksRecord);

        Interlocked.Decrement(ref currentlyRunningTasksCount);
}

我做了计时器回调async 并使用了WhenAll。在每个工作异步函数中,我都有一个控制台输出,它显示活动。现在的问题是,在第二个计时器事件中,每个异步函数由于某种原因工作了两次。定时器设置为长周期。该应用程序是 Windows 控制台类型。是Select 让它运行了两次吗?

【问题讨论】:

  • 尝试在第一次Select(i => ReadSensors) 之后添加ToArray,您可能会在WhenAll 中枚举可枚举两次,在tashsRead.Where 中第二次枚举

标签: c# async-await task-parallel-library


【解决方案1】:

这个:

var tasksRead = Enumerable.Range(3, 35).Select(i => ReadSensorsAsync(i));

创建一个惰性求值的 IEnumerable,它将数字映射到方法调用结果。 ReadSensorsAsync 在这里没有被调用,它在评估期间被调用。

这个 IEnumerable 被计算了两次。这里:

await Task.WhenAll(tasksRead);

这里:

// Here, another lazy IEnumerable is created based on tasksRead.
var tasksRecord = tasksRead.Where(...).Select(...);  
await Task.WhenAll(tasksRecord);  // Here, it is evaluated.

因此,ReadSensorsAsync 被调用了两次。


正如 csharpfolk 在 cmets 中建议的那样,实现 IEnumerable 应该可以解决这个问题:

var tasksRead = Enumerable.Range(3, 35).Select(i => ReadSensorsAsync(i)).ToList();

【讨论】:

  • 所以在await Task.WhenAll(tasksRead) 它开始评估并且因为该方法是可等待的,然后评估下一条语句,其中该函数与之前的调用并行调用。这样对吗?我是否理解正确,ToList 将使事情同步,然后 WhenAll 将不再有意义?
  • 不,这与异步无关!让我们考虑一个简单的同步示例:int i1 = tasksRead.Count(); int i2 = tasksRead.Count(); int i3 = tasksRead.Count(); 将执行 ReadSensorsAsync 三次。
  • 如果有帮助,您可以将taskReads 视为创建传感器读数列表而不是传感器读数结果的函数。如果你多次“调用”它,它会被执行多次。
  • 不使用.ToList(),也可以通过使用await Task.WhenAll(tasksRead); 返回的T[] 来解决以下.Where(,而不是第二次使用tasksRead。此外,从技术上讲,它使用的第二个位置不是tasksRead.Where(,而是await Task.WhenAll(tasksRecord);,因为.Where( 也像第一部分一样被懒惰地评估。
  • @ScottChamberlain:我怎么能错过呢?谢谢,我已将此添加到我的答案中。
【解决方案2】:

当您在 IEnumerable<Task<T>> 上使用 Task.WhenAll 时,它将返回已完成任务结果的 T[]。您需要保存该变量并使用它,否则您将得到多个枚举,例如Henzi mentioned in his answer

这是一个解决方案,无需不必要地调用.ToList()

private static async void TimerCallback(object state)
{
        if (Interlocked.CompareExchange(ref currentlyRunningTasksCount, 1, 0) != 0)
        {
            return;
        }

        var tasksRead = Enumerable.Range(3, 35).Select(i => ReadSensorsAsync(i));
        var finshedTasks = await Task.WhenAll(tasksRead);
        var tasksRecord = finshedTasks.Where(x => x != null).Select(x => RecordReadingAsync(x));
        await Task.WhenAll(tasksRecord);

        Interlocked.Decrement(ref currentlyRunningTasksCount);
}

【讨论】:

    【解决方案3】:

    我想我知道为什么! 简而言之: - 带有 await 隐含功能的函数创建了一个回调线程。更好的是,您可以从 00:17:25 开始在此视频 https://wintellectnow.com/Videos/Watch?videoId=performing-i-o-bound-asynchronous-operations 上看到 Jeffrey Richter 如何解释它

    试试吧:

    var tasksRead = Enumerable.Range(3, 35).Select(i => ReadSensorsAsync(i));
    var tasksRecord = tasksRead.Where(x => x.Result != null).Select(x => RecordReadingAsync(x.Result));
    
    await Task.WhenAll(tasksRead);
    
    await Task.WhenAll(tasksRecord);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-11-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-12-11
      相关资源
      最近更新 更多