Task.RunSynchronously 是用来做什么的?

40

我想知道这个方法的作用是什么?在什么场景下我可以使用这个方法。

我的初步想法是,RunSynchronously 用于调用一个异步方法并在不像 .wait() 那样引起死锁问题的情况下以同步方式运行。

然而,根据MSDN的说明:

  

通常情况下,任务会异步地在线程池线程上执行,并且不会阻塞调用线程。通过调用 RunSynchronously() 方法执行的任务与当前的 TaskScheduler 相关联,并在调用线程上运行。如果目标计划程序不支持在调用线程上运行此任务,则该任务将被安排在计划程序上执行,并且调用线程将被阻塞,直到任务完成执行。

为什么在这里需要一个 TaskScheduler,如果任务将在调用线程上运行呢?


3
从您复制的描述中看来,它似乎会让调度程序决定何时运行它,而不是立即在当前线程上运行它。在这里,调度程序将做出决策,而不仅仅是同步立即运行它。 - Stefano d'Antonio
3
对于大多数异步方法来说,使用RunRunSynchronously是没有用处的,因为它们返回“热”任务(即已经在运行)。你只能在“冷”任务上使用RunRunSynchronously,这些任务还没有开始。 - Damien_The_Unbeliever
5
Stephen Cleary的博客似乎已经涵盖了这个内容。顶部的免责声明很有趣:“本博客文章中绝对没有现代代码推荐的任何内容。如果你正在寻找最佳实践,请离开;这里没有什么好看的。” 因此,它似乎是遗留的。 - Liam
2
在.NET 4.5引入async await流程之前,已经创建了RunSynchronously方法。显然,这个想法是允许使用相同的线程上下文和指定的调度程序同步执行“冷”任务。与.StartNew()一样,它已经过时了。除非您需要在当前线程中创建一个冷任务并同步运行它,否则没有使用它的意义,使用当前的TaskScheduler - Fabjan
1
@Fabjan,对于两者都有一个微小的情况,因为它们都接受TaskScheduler参数。如果您想使用自定义调度程序,例如限制DOP调度程序,则可以使用它们。即使如此,还有更好的选择,比如带有有限DOP的ActionBlock<>。 - Panagiotis Kanavos
显示剩余3条评论
3个回答

26

RunSynchronously委托当前任务调度程序(或传递的参数)决定何时启动任务。

我不确定为什么它存在(可能用于内部或遗留用途),但很难想到在当前版本的.NET中有一个有用的用例。 @Fabjan在问题的评论中提供了一个可能的解释

RunSynchronously请求调度程序以同步方式运行它,但然后调度程序可以完全忽略提示并在线程池线程上运行它,并且您的当前线程将在完成之前同步阻塞。

调度程序不必在当前线程上运行它,也不必立即运行它,尽管我认为常见的调度程序(ThreadPoolTaskScheduler和常见的UI调度程序)会发生这种情况。

RunSynchronously还将在任务已经启动或已完成/故障(这意味着您将无法在异步方法上使用它)时引发异常。

此代码可能会澄清不同的行为:

WaitResult根本不运行任务,它们只等待当前线程上的任务完成并阻塞它,因此如果我们要进行比较,我们可以将StartWaitRunSynchronously进行比较:

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());
    }
}
如果您尝试编写评论RunSynchronously或开始运行代码,则会发现Start会尝试将任务排队到调度程序中,而RunSynchronously将尝试在内联执行它,并在失败时(返回false)仅将其排队。

15

首先让我们看一下这段代码:

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

在这个例子中,我们尝试对一个任务调用RunSynchronously,它包含以下内容:
  • 返回其他带有结果的任务(promise任务)
  • 在线程池线程上运行的'hot'委托任务
  • 另一个由async await创建的promise任务
  • 包含委托的'cold'任务
创建后它们将具有什么状态?
  • WaitingForActivation
  • WaitingToRun
  • WaitingForActivation
  • Created
所有'hot'和promise任务都是以状态 WaitingForActivation 或 WaitingToRun 创建的。'hot'任务还与任务计划程序相关联。 RunSynchronously方法只能处理包含委托并具有状态Created的'cold'任务。
结论:
当没有'hot'任务或者它们没有广泛使用时, RunSynchronously 方法可能已经出现,并且是为了特定目的而创建的。
如果需要自定义 TaskScheduler 来创建'cold'任务,则可以使用它,否则它已过时且几乎无用。
要同步运行“hot”任务(大多数情况下应避免),我们可以使用 task.GetAwaiter().GetResult()。这与 .Result 相同,但是作为奖励,它返回原始异常而不是 AggregateException 。然而,“同步超异步”不是最佳选择,应尽量避免。

值得注意的是,Thread 已经被 Task (Task.Delay()) 取代了,所以 Thread.Sleep 现在也可以被视为遗留代码。这就是为什么它能够与遗留的 RunSynchronously 一起使用的原因。 - Liam
GetAwaiter:此方法旨在供编译器使用,而不是应用程序代码使用。https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.getawaiter - Ares
@Ares:例如,可以参考这个答案,了解task.Result和task.GetAwaiter().GetResult()之间的区别。 - Ulf Åkerstedt

4

Stefano d'Antonio的精彩答案详细描述了RunSynchronously方法的机制。我想添加一些实际例子,说明RunSynchronously实际上很有用。

第一个例子:并行化两个方法调用,同时有效地利用线程:

Parallel.Invoke(() => Method1(), () => Method2());

一个天真的假设可能是Method1Method2将在两个不同的ThreadPool线程上运行,但实际情况并非如此。在幕后,Parallel.Invoke将在当前线程上调用其中一个方法。上面的代码与下面的代码具有相同的行为:
Task task1 = Task.Run(() => Method1());
Task task2 = new(() => Method2());
task2.RunSynchronously(TaskScheduler.Default);
Task.WaitAll(task1, task2);

当当前线程可用时,没有理由将两个调用都卸载到ThreadPool中,因为当前线程可以愉快地参与并行执行。

第二个例子:创建一个代表尚未启动的某些代码的Task表示形式,以便其他异步方法可以await此任务并获取其结果:

Task<DateOnly> task1 = new(() => CalculateDate());
Task<string> task2 = Task.Run(() => GenerateReportAsync(task1));
task1.RunSynchronously(TaskScheduler.Default);
string report = await task2;

我们现在有两个任务同时运行,task1task2,第二个任务依赖于第一个任务的结果。因为没有必要将CalculateDate委派给另一个线程,所以task1在当前线程上同步运行。当前线程可以像任何其他线程一样进行计算。在GenerateReportAsync内部,第一个任务在某个地方被等待,可能会多次等待:

async Task<string> GenerateReportAsync(Task<DateOnly> dateTask)
{
    // Do preliminary stuff
    DateOnly date = await dateTask;
    // Do more stuff
}

await dateTask 可能会立即获得结果(如果此时 dateTask 已经完成),或异步地获得结果(如果 dateTask 还在运行)。无论哪种情况,我们都已经达到了我们想要的目的:将日期的计算并行化,以便生成报告方法中的 // Do preliminary stuff 部分。

我们能否在不使用 Task 构造函数和 RunSynchronously 方法的情况下复制这两个示例的行为?当然可以,使用 TaskCompletionSource 类。但是,不像使用上述技术那样简洁、稳健和描述性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接