Why async functions are called twice?

2020-04-07 19:53发布

I'm using Threading timer to do some periodic job:

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

I made timer call back async and used WhenAll. In each working async function I have one Console output, which shows activity. Now the problem is that on second timer event each async function is working twice for some reason. The timer is set to long period. The application is Windows Console type. Is it Select that somehow make it run twice?

3条回答
甜甜的少女心
2楼-- · 2020-04-07 20:00

I think I know WHY ! In two words: - reason that function with await impliciti create a callback thread. Better you can see how explain it Jeffrey Richter on this video https://wintellectnow.com/Videos/Watch?videoId=performing-i-o-bound-asynchronous-operations from 00:17:25

just try it:

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);
查看更多
ら.Afraid
3楼-- · 2020-04-07 20:03

This:

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

creates a lazily evaluated IEnumerable which maps numbers to method invocation results. ReadSensorsAsync is not invoked here, it will be invoked during evaluation.

This IEnumerable is evaluated twice. Here:

await Task.WhenAll(tasksRead);

and here:

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

Thus, ReadSensorsAsync is invoked twice.


As csharpfolk suggested in the comments, materializing the IEnumerable should fix this:

var tasksRead = Enumerable.Range(3, 35).Select(i => ReadSensorsAsync(i)).ToList();
查看更多
▲ chillily
4楼-- · 2020-04-07 20:10

When you use Task.WhenAll on a IEnumerable<Task<T>> it will return a T[] of the completed Tasks results. You need to save that variable and use it or else you will end up with the multiple enumerations like Henzi mentioned in his answer.

Here is a solution without the unnecessarily calling of .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);
}
查看更多
登录 后发表回答