Stefano d'Antonio's excellent answer describes in details the mechanics of the RunSynchronously
method. I would like to add a couple of practical examples where the RunSynchronously
is actually useful.
First example: Parallelize two method calls, while making efficient use of threads:
Parallel.Invoke(() => Method1(), () => Method2());
A naive assumption might be that the Method1
and Method2
will run on two different ThreadPool
threads, but this is not what's happening. Under the hood the Parallel.Invoke
will invoke one of the two methods on the current thread. The above code has identical behavior with the code below:
Task task1 = Task.Run(() => Method1());
Task task2 = new(() => Method2());
task2.RunSynchronously(TaskScheduler.Default);
Task.WaitAll(task1, task2);
There is no reason to offload both invocations to the ThreadPool
, when there is a thread available right here, the current thread, that can happily participate in the parallel execution.
Second example: Create a Task
representation of some code that has not already started, so that other asynchronous methods can await
this task and get its result:
Task<DateOnly> task1 = new(() => CalculateDate());
Task<string> task2 = Task.Run(() => GenerateReportAsync(task1));
task1.RunSynchronously(TaskScheduler.Default);
string report = await task2;
We now have two tasks running concurrently, the task1
and task2
, and the second task depends on the result of the first. The task1
runs synchronously on the current thread, because there is no reason to offload the CalculateDate
to another thread. The current thread can do the calculation just as well as any other thread. Inside the GenerateReportAsync
, the first task is awaited somewhere, potentially multiple times:
async Task<string> GenerateReportAsync(Task<DateOnly> dateTask)
{
// Do preliminary stuff
DateOnly date = await dateTask;
// Do more stuff
}
The await dateTask
might get the result immediately (in case the dateTask
has completed at that point), or asynchronously (in case the dateTask
is still running). Either way we have achieved what we wanted: to parallelize the calculation of the date, with the // Do preliminary stuff
part of the method that generates the report.
Could we replicate the behavior of these two examples without the Task
constructor and the RunSynchronously
method? Surely, by using the TaskCompletionSource
class. But not as succinctly, robustly and descriptively as by using the techniques shown above.