为什么在控制台应用程序中未捕获默认的SynchronizationContext?

时间:2018-10-07 08:44:00

标签: c# async-await console-application synchronizationcontext

我试图了解有关SynchronizationContext的更多信息,所以我制作了一个简单的控制台应用程序:

private static void Main()
{
    var sc = new SynchronizationContext();
    SynchronizationContext.SetSynchronizationContext(sc);
    DoSomething().Wait();
}

private static async Task DoSomething()
{
    Console.WriteLine(SynchronizationContext.Current != null); // true
    await Task.Delay(3000);
    Console.WriteLine(SynchronizationContext.Current != null); // false! why ?
}

如果我理解正确,await运算符将捕获当前的SynchronizationContext,然后将其余的异步方法发布到该操作符。

但是,在我的应用程序中,SynchronizationContext.Current之后为await为空。为什么会这样?

编辑:

即使我使用自己的SynchronizationContext,也不会捕获它,尽管它的Post函数被调用了。这是我的SC:

public class MySC : SynchronizationContext
{
    public override void Post(SendOrPostCallback d, object state)
    {
        base.Post(d, state);
        Console.WriteLine("Posted");
    }
}

这就是我的用法:

var sc = new MySC();
SynchronizationContext.SetSynchronizationContext(sc);

谢谢!

4 个答案:

答案 0 :(得分:11)

“捕获”一词太不透明了,听起来太像框架了。具有误导性,因为它通常在使用默认SynchronizationContext实现之一的程序中进行。像one you get in a Winforms app。但是,当您编写自己的框架时,该框架将不再有用,而要做它就成了您的工作。

异步/等待管道给上下文提供了在特定线程上运行延续(等待之后的代码)的机会。听起来这是一件微不足道的事情,因为您之前已经做过很多次,但是实际上这很困难。不可能任意中断此线程正在执行的代码,这将导致可怕的重新输入错误。线程必须提供帮助,它需要解决标准producer-consumer problem。采取线程安全队列和清空该队列的循环,以处理调用请求。重写的Post和Send方法的工作是将请求添加到队列中,线程的工作是使用循环将其清空并执行请求。

Winforms,WPF或UWP应用程序的主线程具有这样的循环,它由Application.Run()执行。具有一个知道如何将其与调用请求一起馈入的相应SynchronizationContext,分别是WindowsFormsSynchronizationContext,DispatcherSynchronizationContext和WinRTSynchronizationContext。 ASP.NET也可以做到这一点,使用AspNetSynchronizationContext。所有这些都由框架提供,并由类库管道自动安装。他们在构造函数中捕获同步上下文,并在Post和Send方法中使用Begin / Invoke。

当您编写自己的SynchronizationContext时,现在必须注意这些细节。在您的代码段中,您没有覆盖Post and Send,而是继承了基本方法。他们什么都不知道,只能在任意线程池线程上执行请求。因此,该线程上的SynchronizationContext.Current现在为null,线程池线程不知道请求来自何处。

创建自己的并不难,ConcurrentQueue和委托有助于减少代码。许多程序员都这样做,this library经常被引用。但是要付出巨大的代价,调度程序循环从根本上改变了控制台模式应用程序的行为方式。它阻塞线程,直到循环结束。就像Application.Run()一样。

您需要一种非常不同的编程风格,这是您在GUI应用程序中会熟悉的那种风格。代码不会花费太长时间,因为它会使调度程序循环陷入困境,从而阻止调用请求被调度。在GUI应用程序中,UI变得毫无响应,这很明显,在您的示例代码中,您会注意到您的方法完成很慢,因为该延续不能运行一会儿。您需要一个工作线程来分拆慢速代码,没有免费的午餐。

值得一提的是为什么存在这些东西。 GUI应用程序存在严重问题,其类库永远都不是线程安全的,也无法通过使用lock来使其安全。正确使用它们的唯一方法是从同一线程进行所有调用。如果不这样做,则返回InvalidOperationException。他们的调度程序循环可帮助您完成此任务,从而为Begin / Invoke和async / await提供动力。控制台没有此问题,任何线程都可以向控制台写入内容,而锁可以帮助防止其输出混杂在一起。因此,控制台应用程序不需要自定义SynchronizationContext。 YMMV。

答案 1 :(得分:4)

详细说明已经指出的内容。

您在第一个代码段中使用的SynchronizationContext类是默认实现,它不会执行任何操作。

在第二个代码段中,您创建自己的MySC上下文。但是您缺少使它真正起作用的功能:

public override void Post(SendOrPostCallback d, object state)
{
    base.Post(state2 => {
        // here we make the continuation run on the original context
        SetSynchronizationContext(this); 
        d(state2);
    }, state);        
    Console.WriteLine("Posted");
}

答案 2 :(得分:2)

默认情况下,控制台应用程序和Windows Services中的所有线程仅具有默认的SynchronizationContext。

Kinldy请参阅链接https://msdn.microsoft.com/magazine/gg598924.aspx,以获取更多详细信息。在各种类型的应用程序中,都有关于SynchronizationContext的详细信息。

答案 3 :(得分:0)

实现您自己的 SynchronizationContext 是可行的,但并非微不足道。使用现有实现要容易得多,例如 Nito.AsyncEx.Context 包中的 AsyncContext 类。你可以这样使用它:

using System;
using System.Threading;
using System.Threading.Tasks;
using Nito.AsyncEx;

public static class Program
{
    static void Main()
    {
        AsyncContext.Run(async () =>
        {
            await DoSomethingAsync();
        });
    }

    static async Task DoSomethingAsync()
    {
        Console.WriteLine(SynchronizationContext.Current != null); // True
        await Task.Delay(3000);
        Console.WriteLine(SynchronizationContext.Current != null); // True
    }
}

Try it on Fiddle

AsyncContext.Run 是一种阻塞方法。当提供的异步委托 Func<Task> action 完成时,它将完成。所有异步延续都将在控制台应用程序的主线程上运行,前提是没有 Task.RunConfigureAwait(false) 会强制您的代码退出上下文。

在控制台应用程序中使用单线程 SynchronizationContext 的后果是:

  1. 您将不再需要担心线程安全,因为您的所有代码都将集中到一个线程中。
  2. 您的代码容易受到 deadlocks 的影响。您代码中的任何 .Wait().Result.GetAwaiter().GetResult() 等都很可能导致您的应用程序冻结,在这种情况下,您必须从 Windows 任务管理器手动终止该进程。