1

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?

Atif Aziz
  • 36,108
  • 16
  • 64
  • 74
Vlad5Maxed
  • 13
  • 2

2 Answers2

2

The concept of the TaskScheduler was devised before the advent of the async/await, and it ended up not being compatible with it. You can see an experiment that demonstrates this incompatibility here: How to run a Task on a custom TaskScheduler using await?

The abstraction that is available for controlling the behavior of async/await is the SynchronizationContext. It is quite similar to a TaskScheduler. So much actually that some people have been wondering why we need both: What is the conceptual difference between SynchronizationContext and TaskScheduler.

If you are interested for something like a SingleThreadSynchronizationContext, you can find an implementation here: Await, SynchronizationContext, and Console Apps

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
2

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810