39

I just wonder what's the method for? In what kind of scenario I can use this method.

My initial thought is RunSynchronously is for calling an async method and running that synchronously without causing a deadlock issue like what .wait() does.

However, according to MSDN,

Ordinarily, tasks are executed asynchronously on a thread pool thread and do not block the calling thread. Tasks executed by calling the RunSynchronously() method are associated with the current TaskScheduler and are run on the calling thread. If the target scheduler does not support running this task on the calling thread, the task will be scheduled for execution on the schedule, and the calling thread will block until the task has completed execution

Why need a TaskScheduler here, if the task going to run on the calling thread?

ValidfroM
  • 2,626
  • 3
  • 30
  • 41
  • Most like due to "If the target scheduler does not support running this task on the calling thread, the task will be scheduled for execution on the scheduler, and the calling thread will block until the task has completed execution". It *may* run on the same thread, it doesn't *have to* – Camilo Terevinto Oct 11 '18 at 12:05
  • 3
    Looks like it will let the scheduler decide when to run it rather than running it immediately on the current thread from the description you copied. Scheduler will make a decision here instead of just running it synchronously immediately. – Stefano d'Antonio Oct 11 '18 at 12:06
  • Bottom line is that it will *definitely* block the calling thread and execute the task in the same thread if possible or in any other available thread if it's not. In the meantime the caller thread will be blocked. – Fabjan Oct 11 '18 at 12:13
  • 3
    It wouldn't be of any use with most async methods since they return "hot" Tasks (i.e. already running) You can only use `Run` or `RunSynchronously` with "cold" Tasks that haven't started yet. – Damien_The_Unbeliever Oct 11 '18 at 12:19
  • 5
    [Stephen Cleary blog](https://blog.stephencleary.com/2015/02/a-tour-of-task-part-8-starting.html) seems to have it covered. The disclaimer at the top is interesting *There is absolutely nothing in this blog post that is recommended for modern code. If you’re looking for best practices, move along; there’s nothing to see here.* So it seems legacy – Liam Oct 11 '18 at 12:20
  • @Fabjan It all makes sense, but why MS creates this method. If it is just like `.Result()` or `.Wait()`. Do I need to worry about deadlock issue here? As the task potentially will have a context synchronization. – ValidfroM Oct 11 '18 at 12:24
  • 2
    Method `RunSynchronously` was created before `async await` flow was introduced in .NET 4.5, apparently the idea was to allow executing of 'cold' tasks synchronously with an attempt of using the same thread context, and specified scheduler. Same as `.StartNew()` it's obsolete. There is no point in using it unless you need to create a cold task and run it in a current thread synchronously, using the current `TaskScheduler`. – Fabjan Oct 11 '18 at 12:37
  • 1
    @Fabjan there's one tiny case for both, since they accept a TaskScheduler parameter. If you want to use a custom scheduler, eg a limeted DOP scheduler. Even then, there are better options like an ActionBlock<> with a limited DOP – Panagiotis Kanavos Oct 11 '18 at 13:08

3 Answers3

25

RunSynchronously delegates the decision of when to start the task to the current task scheduler (or the one passed as argument).

I am not sure why it is there (maybe for internal or legacy use), but it is hard to think of a useful use case in the current versions of .NET. @Fabjan has a possible explanation in his comment to the question.

RunSynchronously asks the scheduler to run it synchronously but then the scheduler could very well ignore the hint and run it in a thread pool thread and your current thread will synchronously block until it is completed.

The scheduler does not have to run it on the current thread and does not have to run it immediately although I think it is what will happen on common schedulers (ThreadPoolTaskScheduler and common UI schedulers).

RunSynchronously will also throw an exception if the task has already been started or is completed/faulted (this means you will not be able to use it on async methods).

This code may clarify the different behaviour:

Wait and Result don't run the task at all, they just wait for the task completion on the current thread and block it until the completion so if we want to compare, we can compare Start and Wait to RunSynchronously:

class Scheduler : TaskScheduler
{
    protected override void QueueTask(Task task) => 
        Console.WriteLine("QueueTask");

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        Console.WriteLine("TryExecuteTaskInline");

        return false;
    }

    protected override IEnumerable<Task> GetScheduledTasks() => throw new NotImplementedException();
}

static class Program
{
    static void Main()
    {
        var taskToStart = new Task(() => { });
        var taskToRunSynchronously = new Task(() => { });

        taskToStart.Start(new Scheduler());
        taskToRunSynchronously.RunSynchronously(new Scheduler());
    }
}

If you try and comment Start or RunSynchronously and run the code, you will see that Start tries and queue the task to the scheduler while RunSynchronously will try and execute it inline and if failing (return false), it will just queue it.

Liam
  • 27,717
  • 28
  • 128
  • 190
Stefano d'Antonio
  • 5,874
  • 3
  • 32
  • 45
14

First let's have a look into this code:

public async static Task<int> MyAsyncMethod()
{
   await Task.Delay(100);
   return 100;
}

//Task.Delay(5000).RunSynchronously();                        // bang
//Task.Run(() => Thread.Sleep(5000)).RunSynchronously();     // bang
// MyAsyncMethod().RunSynchronously();                      // bang

var t = new Task(() => Thread.Sleep(5000));
t.RunSynchronously();                                     // works

In this example we've tried to invoke RunSynchronously on task that:

  • Returns other Task with result (promise task)
  • A 'hot' delegate task that runs the on a threadpool thread
  • Another promise task created by async await
  • 'Cold' task with a delegate

What statuses will they have after creation ?

  • WaitingForActivation
  • WaitingToRun
  • WaitingForActivation
  • Created

All 'hot' and promise tasks are created with status WaitingForActivationor WaitingToRun. Hot tasks are also associated with a task scheduler.

Method RunSynchronously only knows how to work with 'cold' tasks that contain delegate and has status Created.

Conclusion:

Method RunSynchronously had probably appeared when there were no 'hot' tasks or they hadn't been extensively used and was created for a specific purpose.

We might want to use it in case when we need a 'cold' task with custom TaskScheduler, otherwise it is outdated and pretty useless.

For running a 'hot' task synchronously (which we should avoid mostly) we could use task.GetAwaiter().GetResult(). This works same as .Result but as a bonus it returns original exception instead of AggregateException. Still, 'sync over async' is not the best choice and should be avoided if possible.

Fabjan
  • 13,506
  • 4
  • 25
  • 52
  • It's probably worth noting that `Thread` has (basically) been superseded with `Task` (`Task.Delay()`) so `Thread.Sleep` could be considered legacy too now. Which is why it works with the legacy `RunSynchronously` – Liam Oct 11 '18 at 13:27
  • GetAwaiter: This method is intended for compiler use rather than for use in application code. https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.getawaiter – Ares Mar 18 '20 at 15:23
  • @Ares: See for instance [this answer](https://stackoverflow.com/a/38530225/1298001) regarding task.Result vs task.GetAwaiter().GetResult(). – Ulf Åkerstedt Nov 12 '20 at 12:51
1

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.

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