Recently I found out about custom task schedulers and I wanted to blog about all the wonderful things you can do with them. I also imagined new ways of doing await/async by tweaking task schedulers. After hours of attempts, I've come to the conclusion that custom task schedulers are incompatible with await/async and should not be used. Here is why:
- a task scheduler is used to execute synchronous code inside tasks while async/await code is already asynchronous
- while async/await code is transformed by the compiler into a state machine with the code that follows being turned into a task that is scheduled on TaskScheduler.Current, the state machine has nothing to do with the task scheduler (see Dissecting the async methods in C#)
- there are no methods that are both aware of await/async code and a custom task scheduler; by design they are incompatible (see Task.Run vs Task.Factory.StartNew)
- while a stubborn developer could reproduce the functionality of Task.Run and specify a custom task scheduler, or detect tasks that return tasks and Unwrap them, there are easier and safer ways of doing the same thing without a custom task scheduler
- as the scheduler will be used not only by the tasks run by the developer, but also by the code separated by await boundaries, the results will be unpredictable except the most simple of scenarios
And a pretty diagram from Microsoft representing the order of the operations and how complex they are. It's not just a case of method executed somewhere, but a complex flow that uses the ThreadPoolTaskScheduler as the default task scheduler as a fundamental low level functionality that should not be changed.
If you need more convincing, consider that the code after an await instruction may not even execute on the same thread (or indeed thread pool) as the one before, even if as written appears part of the same method (see async - stay on the current thread? for more details). More on thread pools from Jon Skeet here: The Thread Pool and Asynchronous Methods.