我试图了解有关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);
谢谢!
答案 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
}
}
AsyncContext.Run
是一种阻塞方法。当提供的异步委托 Func<Task> action
完成时,它将完成。所有异步延续都将在控制台应用程序的主线程上运行,前提是没有 Task.Run
或 ConfigureAwait(false)
会强制您的代码退出上下文。
在控制台应用程序中使用单线程 SynchronizationContext
的后果是:
.Wait()
、.Result
、.GetAwaiter().GetResult()
等都很可能导致您的应用程序冻结,在这种情况下,您必须从 Windows 任务管理器手动终止该进程。