SynchronizationContext做什么?


135

在《 Programming C#》一书中,有一些有关SynchronizationContext以下示例代码:

SynchronizationContext originalContext = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(delegate {
    string text = File.ReadAllText(@"c:\temp\log.txt");
    originalContext.Post(delegate {
        myTextBox.Text = text;
    }, null);
});

我是主题初学者,所以请详细回答。首先,我不知道上下文是什么意思,程序保存在中originalContext什么?当该Post方法被触发时,UI线程将做什么?
如果我问一些愚蠢的事情,请指正我,谢谢!

编辑:例如,如果我只是myTextBox.Text = text;在方法中写什么,有什么区别?


1
精美的手册中有这样说 :此类实现的同步模型的目的是允许公共语言运行时的内部异步/同步操作在不同的同步模型下正确运行。此模型还简化了托管应用程序必须遵循的一些要求,以便在不同的同步环境下正常工作。
ta.speot。是

恕我直言,异步正在等待这样做
Royi Namir

7
@RoyiNamir:是的,但是猜猜是什么:async/ await依赖于SynchronizationContext下面。
stakx-

Answers:


170

SynchronizationContext做什么?

简而言之,SynchronizationContext表示可能在其中执行代码的位置。传递给其SendPost方法的委托将在该位置被调用。(Post是的非阻塞/异步版本Send。)

每个线程都可以有一个SynchronizationContext与之关联的实例。可以通过调用static SynchronizationContext.SetSynchronizationContext方法将运行线程与同步上下文关联,并且可以通过SynchronizationContext.Current属性查询运行线程的当前上下文。

尽管我刚刚写了什么(每个线程都有一个关联的同步上下文),a SynchronizationContext不一定代表一个特定的线程;它还可以将传递给它的委托的调用转发到多个线程中的任何一个(例如,到ThreadPool工作线程),或者(至少在理论上)转发到特定的CPU内核,甚至是另一个网络主机。您的代表最终运行的位置取决于所使用的类型SynchronizationContext

Windows窗体将WindowsFormsSynchronizationContext在创建第一个窗体的线程上安装一个。(此线程通常称为“ UI线程”。)这种类型的同步上下文将完全在该线程上调用传递给它的委托。这是非常有用的,因为Windows窗体与许多其他UI框架一样,仅允许在创建控件的同一线程上操纵控件。

如果我只写myTextBox.Text = text;方法怎么办?有什么区别?

您传递给的代码ThreadPool.QueueUserWorkItem将在线程池工作线程上运行。也就是说,它不会在myTextBox创建您的线程上执行,因此Windows Forms迟早(尤其是在Release版本中)将引发异常,告诉您可能无法myTextBox从另一个线程进行访问。

这就是为什么您必须myTextBox在特定分配之前以某种方式将其从工作线程“切换回”到“ UI线程”(创建位置)。这样做如下:

  1. 当您仍在UI线程上时,在其中捕获Windows窗体SynchronizationContext,并将对其的引用存储在变量(originalContext)中,以备后用。此时您必须查询SynchronizationContext.Current;如果在传递给的代码中对其进行查询,则ThreadPool.QueueUserWorkItem可能会获得与线程池的工作线程相关联的任何同步上下文。一旦存储了对Windows Forms上下文的引用,就可以随时随地使用它来将代码“发送”到UI线程。

  2. 每当您需要操纵UI元素时(但现在不在或可能不在UI线程上),都可以通过访问Windows窗体的同步上下文originalContext,并将将操纵UI的代码移交给SendPost


最后的评论和提示:

  • 同步上下文对您不起作用的是,告诉您哪些代码必须在特定的位置/上下文中运行,以及哪些代码可以正常执行而无需将其传递给SynchronizationContext。为了确定这一点,您必须了解要针对其进行编程的框架的规则和要求-在这种情况下为Windows窗体。

    因此,请记住Windows窗体的这一简单规则:请勿从创建线程的线程之外的其他线程访问控件或窗体。如果必须执行此操作,请使用SynchronizationContext如上所述的机制,或者Control.BeginInvoke(这是Windows Forms特定的方式来执行完全相同的操作)。

  • 如果你对编程.NET 4.5或更高版本,可以通过转换代码是明确地让你的生活更容易使用SynchronizationContextThreadPool.QueueUserWorkItemcontrol.BeginInvoke等转移到新async/ await关键字任务并行库(TPL) ,即围绕API在TaskTask<TResult>类。这些将在很大程度上照顾捕获UI线程的同步上下文,启动异步操作,然后返回到UI线程,以便您可以处理该操作的结果。


您说Windows窗体与许多其他UI框架一样,仅允许在同一线程上操纵控件,但是Windows中的所有窗口必须由创建它的同一线程访问。
user34660 '17

4
@ user34660:不,那是不正确的。您可以具有创建Windows窗体控件的多个线程。但是每个控件都与创建它的那个线程相关联,并且只能由那个线程访问。来自不同UI线程的控件之间的交互方式也非常有限:一个控件不能成为另一个控件的父/子,它们之间的数据绑定是不可能的,等等。最后,每个创建控件的线程都需要自己的消息循环(由Application.RunIIRC 开始)。这是一个相当高级的主题,而不是随便做的。
stakx-不再贡献

我的第一个评论是由于您说“像许多其他UI框架一样”,这意味着某些窗口允许来自不同线程的“控件的操纵”,但Windows窗口却没有。您不能为同一窗口 “具有创建Windows窗体控件的多个线程”,并且“必须由同一线程访问”和“必须仅由该一个线程访问”在说同样的话。我怀疑是否可以为同一窗口创建“来自不同UI线程的控件”。对于那些在.Net之前具有Windows编程经验的人来说,所有这些都不是高级的。
user34660

3
所有关于“ Windows”和“ Windows Windows”的话题都让我感到头晕。我有没有提到这些“窗户”?我不这么认为...
stakx-不再贡献

1
@ibubi:我不确定我是否理解你的问题。任何线程的同步上下文都不是set(null)或的实例SynchronizationContext(或其子类)。引用的重点不是您得到的,而是您不会得到的:UI线程的同步上下文。
stakx-不再

24

我想添加到其他答案中,SynchronizationContext.Post只是将一个回调排队,以便稍后在目标线程上执行(通常在目标线程的消息循环的下一个周期内),然后在调用线程上继续执行。另一方面,SynchronizationContext.Send尝试立即在目标线程上执行回调,这会阻塞调用线程并可能导致死锁。在这两种情况下,都有可能发生代码重入(在返回对同一方法的先前调用之前,在同一执行线程上输入类方法)。

如果您熟悉Win32编程模型,则将非常类似于API PostMessageSendMessageAPI,您可以调用它们来从与目标窗口不同的线程中调度消息。

这是什么是同步上下文的很好的解释: 都是关于SynchronizationContext的


16

它存储同步提供程序,该类是从SynchronizationContext派生的。在这种情况下,它可能是WindowsFormsSynchronizationContext的一个实例。该类使用Control.Invoke()和Control.BeginInvoke()方法来实现Send()和Post()方法。也可以是DispatcherSynchronizationContext,它使用Dispatcher.Invoke()和BeginInvoke()。在Winforms或WPF应用程序中,创建窗口后,即会自动安装该提供程序。

当您在另一个线程(如代码片段中使用的线程池线程)上运行代码时,必须注意不要直接使用线程不安全的对象。像任何用户界面对象一样,您必须从创建TextBox的线程中更新TextBox.Text属性。Post()方法可确保委托目标在该线程上运行。

请注意,此代码段有些危险,只有当您从UI线程调用它时,它才能正常工作。SynchronizationContext.Current在不同的线程中具有不同的值。仅UI线程具有可用值。这就是代码必须复制它的原因。在Winforms应用中,一种更易读,更安全的方法:

    ThreadPool.QueueUserWorkItem(delegate {
        string text = File.ReadAllText(@"c:\temp\log.txt");
        myTextBox.BeginInvoke(new Action(() => {
            myTextBox.Text = text;
        }));
    });

它具有从任何线程调用时都可以工作的优点。使用SynchronizationContext.Current的优点是,无论代码是在Winforms还是WPF中使用,它仍然可以工作,而在库中则很重要。这当然不是此类代码的好例子,您总是知道这里使用的是哪种TextBox,因此您始终知道是使用Control.BeginInvoke还是Dispatcher.BeginInvoke。实际上使用SynchronizationContext.Current并不常见。

这本书试图教您有关线程的知识,因此使用这个有缺陷的示例是可以的。在现实生活中,在少数情况下,您可能考虑使用SynchronizationContext.Current,但仍将其留给C#的async / await关键字或TaskScheduler.FromCurrentSynchronizationContext()来替您完成。但是请注意,由于完全相同的原因,当您在错误的线程上使用它们时,它们仍然表现出片段的方式。在此处,这是一个非常常见的问题,额外的抽象级别很有用,但很难弄清为什么它们不能正常工作。希望这本书还会告诉您什么时候不使用它:)


抱歉,为什么让UI线程句柄是线程安全的?即我认为,在Post()触发时,UI线程可能正在使用myTextBox,这样安全吗?
cloudFan 2013年

4
您的英语很难解读。您的原始代码段仅在从UI线程调用时才能正常工作。这是很常见的情况。只有这样才能将其发布回UI线程。如果从辅助线程调用它,则Post()委托目标将在线程池线程上运行。Kaboom。这是您想自己尝试的东西。启动一个线程,并让该线程调用此代码。如果代码由于NullReferenceException而崩溃,则您做对了。
汉斯·帕桑

5

此处的同步上下文的目的是确保myTextbox.Text = text;在主UI线程上被调用。

Windows要求GUI控件只能由创建它们的线程访问。如果您尝试在不首先进行同步的情况下(通过多种方法中的任何一种,例如this或Invoke模式)在后台线程中分配文本,则将引发异常。

这是在创建后台线程之前保存同步上下文,然后后台线程使用context.Post方法执行GUI代码。

是的,您显示的代码基本上没有用。为什么要创建后台线程,而只需要立即返回主UI线程呢?这只是一个例子。


4
“是的,您显示的代码基本上是无用的。为什么创建一个后台线程,而只需要立即返回到主UI线程呢?这只是一个例子。” -如果文件很大,则从文件中读取数据可能会是一项艰巨的任务,这可能会阻塞UI线程并使其无法响应
Yair Nevet '16

我有一个愚蠢的问题。每个线程都有一个ID,例如,我想UI线程也有一个ID = 2。然后,当我在线程池线程上时,可以执行以下操作:var thread = GetThread(2); thread.Execute(()=> textbox1.Text =“ foo”)吗?
约翰,

@John-不,我认为这不起作用,因为线程已经在执行。您无法执行已经执行的线程。仅当线程未运行(IIRC)时,执行才有效
Erik Funkenbusch '19

3

到源头

每个线程都有与之关联的上下文(也称为“当前”上下文),并且可以在线程之间共享这些上下文。ExecutionContext包含正在执行程序的当前环境或上下文的相关元数据。SynchronizationContext表示一个抽象-它表示应用程序代码的执行位置。

通过SynchronizationContext,您可以将任务排队到另一个上下文中。请注意,每个线程可以具有自己的SynchronizatonContext。

例如:假设您有两个线程,Thread1和Thread2。假设Thread1正在做一些工作,然后Thread1希望在Thread2上执行代码。一种可行的方法是向Thread2询问其SynchronizationContext对象,将其提供给Thread1,然后Thread1可以调用SynchronizationContext.Send在Thread2上执行代码。


2
同步上下文不一定要绑定到特定线程。多个线程可能正在处理对单个同步上下文的请求,单个线程可能正在处理对多个同步上下文的请求。
Servy '17

3

SynchronizationContext为我们提供了一种从其他线程更新UI的方法(通过Send方法同步或通过Post方法异步)。

看下面的例子:

    private void SynchronizationContext SyncContext = SynchronizationContext.Current;
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Thread thread = new Thread(Work1);
        thread.Start(SyncContext);
    }

    private void Work1(object state)
    {
        SynchronizationContext syncContext = state as SynchronizationContext;
        syncContext.Post(UpdateTextBox, syncContext);
    }

    private void UpdateTextBox(object state)
    {
        Thread.Sleep(1000);
        string text = File.ReadAllText(@"c:\temp\log.txt");
        myTextBox.Text = text;
    }

SynchronizationContext.Current将返回UI线程的同步上下文。我怎么知道 在每个表单或WPF应用程序的开始处,将在UI线程上设置上下文。如果创建一个WPF应用程序并运行我的示例,您将看到单击按钮时,它将休眠大约1秒钟,然后它将显示文件的内容。您可能会希望它不会,因为UpdateTextBox方法(即Work1)的调用者是传递给Thread的方法,因此它应该使该线程休眠,而不是主UI线程NOPE!即使将Work1方法传递给线程,也请注意,它也接受SyncContext对象。如果您查看它,将会看到UpdateTextBox方法是通过syncContext.Post方法而不是Work1方法执行的。看一下以下内容:

private void Button_Click(object sender, RoutedEventArgs e) 
{
    Thread.Sleep(1000);
    string text = File.ReadAllText(@"c:\temp\log.txt");
    myTextBox.Text = text;
}

最后一个示例与此示例执行相同。两者在工作时都不会阻止UI。

总之,将SynchronizationContext视为一个线程。它不是线程,它定义了一个线程(请注意,并非所有线程都具有SyncContext)。每当我们在其上调用Post或Send方法来更新UI时,就如同通常从主UI线程中更新UI一样。如果由于某些原因需要从其他线程更新UI,请确保该线程具有主UI线程的SyncContext并仅使用要执行的方法调用该方法上的Send或Post方法组。

希望这对您有帮助,队友!


2

SynchronizationContext本质上是回调委托执行的提供者,主要负责确保在特定代码部分(封装在.Net TPL的Task obj中)之后,委托在给定的执行上下文中运行。完成。

从技术角度来看,SC是一个简单的C#类,旨在支持和提供其专门用于任务并行库对象的功能。

除控制台应用程序外,每个.Net应用程序都基于特定的基础框架(例如WPF,WindowsForm,Asp Net,Silverlight等)具有此类的特​​定实现。

此对象的重要性与异步执行代码返回的结果与等待该异步工作的结果的从属代码的执行之间的同步密切相关。

“上下文”一词代表执行上下文,即等待代码将在其中执行的当前执行上下文,即异步代码与其等待代码之间的同步发生在特定的执行上下文中,因此该对象称为SynchronizationContext:它表示异步代码同步和等待代码执行后的执行上下文


1

该示例来自Joseph Albahari的Linqpad示例,但确实有助于理解同步上下文的功能。

void WaitForTwoSecondsAsync (Action continuation)
{
    continuation.Dump();
    var syncContext = AsyncOperationManager.SynchronizationContext;
    new Timer (_ => syncContext.Post (o => continuation(), _)).Change (2000, -1);
}

void Main()
{
    Util.CreateSynchronizationContext();
    ("Waiting on thread " + Thread.CurrentThread.ManagedThreadId).Dump();
    for (int i = 0; i < 10; i++)
        WaitForTwoSecondsAsync (() => ("Done on thread " + Thread.CurrentThread.ManagedThreadId).Dump());
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.