为啥控制台应用程序中没有捕获默认的 SynchronizationContext?

Posted

技术标签:

【中文标题】为啥控制台应用程序中没有捕获默认的 SynchronizationContext?【英文标题】:Why the default SynchronizationContext is not captured in a Console App?为什么控制台应用程序中没有捕获默认的 SynchronizationContext? 【发布时间】:2019-03-12 05:10:04 【问题描述】:

我正在尝试更多地了解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.Currentawait 之后为空。这是为什么呢?

编辑:

即使我使用自己的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);

谢谢!

【问题讨论】:

Link:首先尝试获取当前同步上下文。如果当前上下文实际上只是基本 SynchronizationContext 类型,其目的是等同于根本没有当前 SynchronizationContext,则忽略它。这通过避免不必要的帖子和工作项排队来帮助提高性能,但更重要的是,它确保如果代码碰巧将默认上下文发布为当前,它不会阻止使用当前任务调度程序(如果有的话)。 P.S.您的 Post 实现不正确。它必须确保在适当的上下文中调用委托。 base.Post 不是为你做的。 @PetSerAl "如果当前上下文真的只是基本 SynchronizationContext 类型..."MySC 不是基本 SC 类型;表示syncCtx.GetType() != typeof(SynchronizationContext) 为真。 @PetSerAl 是的,我的Post 不正确.. 这只是用于学习目的的假实现:) 正如你所说,你的Post 被调用了。这就是上下文捕获结束的地方。它只负责在捕获的上下文中调用Post,没有别的。其他任何事情都是 Post 实施者的责任。 【参考方案1】:

“捕获”这个词太不透明了,听起来太像框架应该做的事情。误导,因为它通常在使用默认 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 时,您现在必须注意这些细节。在您的 sn-p 中,您没有覆盖 Post 和 Send,而是继承了基本方法。他们一无所知,只能在任意线程池线程上执行请求。所以 SynchronizationContext.Current 现在在该线程上为空,线程池线程不知道请求来自哪里。

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

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

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

【讨论】:

【参考方案2】:

默认情况下,控制台应用程序和 Windows 服务中的所有线程只有默认的SynchronizationContext

请参考 MSDN 文章Parallel Computing - It's All About the SynchronizationContext。这有关于SynchronizationContexts 在各种类型的应用程序中的详细信息。

【讨论】:

【参考方案3】:

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

您在第一个代码 sn-p 中使用的 SynchronizationContext 类是 default 实现,它不做任何事情。

在第二个代码 sn-p 中,您创建自己的 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");

【讨论】:

但是等待之后的代码还在另一个线程中继续,为什么会这样?【参考方案4】:

实现您自己的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 的后果是:

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

【讨论】:

以上是关于为啥控制台应用程序中没有捕获默认的 SynchronizationContext?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Firebug 不为未定义的属性显示“未捕获的类型错误”?

为啥 xcode 编辑方案中的 GPU 帧捕获中没有 GLES 选项

为啥我的 WPF KeyDown 处理程序没有捕获 CTRL+A?

为啥在这个多线程示例中没有捕获到异常?

为啥我的 Axios 实例没有在捕获的错误中返回响应?

为啥使用Try,Catch捕获异常,程序依然Crash