'How to make ConcurrentExclusiveSchedulerPair work with async and await

I have following code:

static async Task Main()
{
    ConcurrentExclusiveSchedulerPair concurrentExclusiveSchedulerPair = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default, 4);
    var factory = new TaskFactory(concurrentExclusiveSchedulerPair.ConcurrentScheduler);
    for (int i = 0; i < 10; i++)
    {
        factory.StartNew(ThreadTask);
    }

    concurrentExclusiveSchedulerPair.Complete();

    await concurrentExclusiveSchedulerPair.Completion;
    Console.WriteLine("Completed");
}

private static async Task ThreadTask()
{
    var random = new Random();
    await Task.Delay(random.Next(100, 200));
    Console.WriteLine($"Finished {Thread.CurrentThread.ManagedThreadId}");
}

and program finishes executing before tasks are completed. I understand why does it happens as ThreadTask returns completed task and from ConcurrentExclusiveSchedulerPair point of view it does finish executing. I also know few workarounds but is there any correct way to run this pattern with async?



Solution 1:[1]

TaskScheduler is compatible with async/await, but some of the behavior can be surprising.

When await captures its context, it captures the current SynchronizationContext unless it is null, in which case it captures the current TaskScheduler. So, ThreadTask will begin executing with the concurrent scheduler, and after its await it will resume on that same concurrent scheduler.

However, the semantics can be surprising, because the way async works with a task scheduler is that the async method is split into multiple tasks. Each await is a "split" point where the method is broken up. And only those smaller tasks are what is actually scheduled by the TaskScheduler.

So in your case, your code is starting 10 ThreadTask invocations, each running on the concurrent scheduler, and each of them hits an await point. Then the code calls Complete on the scheduler, which tells it not to accept any more tasks. Then when the awaits complete, they schedule the async method continuation (as a task) to that task scheduler, which as already been completed.

So, while technically await was designed to work with TaskScheduler, in practice few people use it that way. In particular, the "concurrent" and "exclusive" task schedulers can have surprising semantics, since a method that is suspended due to an await does not count as "running" to a task scheduler.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Stephen Cleary