为什么异步函数会被调用两次?

5

我正在使用线程定时器来执行一些定期的工作:

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导致它运行两次吗?


1
尝试在第一个 Select(i => ReadSensors) 后添加 ToArray,这样你可能会枚举两次可枚举对象,一次在 WhenAll 中,另一次在 tashsRead.Where 中。 - csharpfolk
1
可能是异步任务被评估两次的重复问题。 - Charles Mager
3个回答

12

这个:

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在评论中建议的那样,将IEnumerable材料化应该可以解决这个问题:
var tasksRead = Enumerable.Range(3, 35).Select(i => ReadSensorsAsync(i)).ToList();

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

2
当您在IEnumerable<Task<T>>上使用Task.WhenAll时,它将返回已完成任务结果的T[]。您需要保存该变量并使用它,否则您将会得到多个枚举,就像Henzi在他的答案中提到的那样
这是一种解决方案,不需要不必要地调用.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);
}

0

我想我知道为什么! 简而言之:使用带有await的函数会创建一个回调线程。你最好看一下Jeffrey Richter在这个视频https://wintellectnow.com/Videos/Watch?videoId=performing-i-o-bound-asynchronous-operations中的解释,从00:17:25开始。

试试吧:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接