SynchronizationContext做什么?

在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中的originalContext什么? 当Post方法被触发时,UI线程会做什么?
如果我问一些愚蠢的事情,请纠正我,谢谢!

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

SynchronizationContext做什么?

简而言之, SynchronizationContext表示一个位置“where”代码可能被执行。 然后将在该位置调用传递给其SendPost方法的委托。 ( PostSend的非阻塞/asynchronous版本。)

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

尽pipe我刚刚写了(每个线程都有一个关联的同步上下文),但SynchronizationContext并不一定代表一个特定的线程 , 它还可以将传递给它的委托的调用转发给几个线程中的任何一个 (例如一个ThreadPool工作线程),或者(至less在理论上)转移到一个特定的CPU核心 ,甚至转移到另一个networking主机 。 代表最终运行的地方取决于所使用的SynchronizationContext的types。

Windows窗体将在创build第一个窗体的线程上安装WindowsFormsSynchronizationContext 。 (这个线程通常被称为“UI线程”)。这种types的同步上下文调用在该线程上传递给它的委托。 这是非常有用的,因为像许多其他UI框架一样,Windows窗体只允许在创build它们的同一个线程上操作控件。

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

您传递给ThreadPool.QueueUserWorkItem的代码将在线程池工作线程上运行。 也就是说,它不会在创buildmyTextBox的线程上执行,所以Windows Forms迟早会(特别是在Release版本中)抛出一个exception,告诉你不能从另一个线程访问myTextBox

这就是为什么你必须以某种方式从工作者线程切换到“UI线程”(其中创buildmyTextBox )在该特定分配之前。 这是如下完成的:

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

  2. 无论何时您需要操作UI元素(但不是或不可以在UI线程上),请通过originalContext访问Windows窗体的同步上下文,并将用于操作UI的代码交给“ Send或“ Post


最后的评论和提示:

  • 什么同步上下文不会为你做的是告诉你哪个代码必须在特定的位置/上下文中运行,哪些代码可以正常执行,而不用传递给SynchronizationContext 。 为了做出决定,你必须知道你正在编程的框架的规则和要求 – 在这种情况下Windows窗体。

    所以记住这个Windows窗体的简单规则:不要从创build它们的线程访问控件或窗体。 如果你必须这样做,可以使用上面描述的SynchronizationContext机制,或者使用Control.BeginInvoke (这是一个Windows窗体特定的方法来完成同样的事情)。

  • 如果您使用.NET 4.5或更高版本进行编程,您可以将显式使用SynchronizationContextThreadPool.QueueUserWorkItemcontrol.BeginInvoke等的代码转换为新的async / await关键字 ,并将任务并行库(TPL) ,即围绕TaskTask<TResult>类的API。 这些将在很大程度上处理捕获UI线程的同步上下文,启动asynchronous操作,然后返回到UI线程,以便处理操作的结果。

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

如果您熟悉Win32编程模型,则可以使用PostMessageSendMessage API进行非常类似的比较,您可以调用它来从与目标窗口不同的线程调度消息。

下面是同步上下文的一个很好的解释: 它是关于SynchronizationContext的 。

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

当你在另一个线程上运行代码时,比如代码片段中使用的线程池线程,你必须小心不要直接使用线程不安全的对象。 像任何用户界面对象一样,您必须从创buildTextBox的线程更新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并不常见。

这本书试图教你关于线程,所以使用这个有缺陷的例子是好的。 在现实生活中,在less数情况下你可能会考虑使用SynchronizationContext.Current,你仍然可以将它保留在C#的async / await关键字或TaskScheduler.FromCurrentSynchronizationContext()中。 但是请注意,当您在错误的线程中使用它们时,仍然会错误地执行片段的操作,原因完全相同。 这里有一个非常常见的问题,额外的抽象层次是有用的,但是很难弄清楚为什么他们不能正常工作。 希望这本书也告诉你什么时候不用它:)

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

Windows要求GUI控件只能由它们创build的线程来访问。 如果您尝试在后台线程中分配文本而没有首先进行同步(通过几种方法中的任何一种,例如this或Invoke模式),则会抛出exception。

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

是的,你显示的代码基本上是无用的。 为什么要创build一个后台线程,只能立即需要回到主UI线程? 这只是一个例子。

你和你的妻子正在为单独的人送单独的礼物。 你正在送你的父亲,她正在送她的妈妈。 你准备你的包裹,并把它放在门廊。 (线程0)你想通过联邦快递(线程1),她希望通过UPS(线程2)。 你们都希望从同一个人送到你家的通知。 (同步上下文)。 他抓包,并通过Fedex和UPS发送。 最终,这个人不应该把通知发到你的公司地址,因为他不能进入build筑物(访问违规,他应该回到被叫的地方)。 所以,一旦你的包裹交付,他回到你的家庭地址,并通知包交付。

线程1和线程2之间的资源共享比较复杂。 上面的scenerio是networking调用中最基本的用法。

来源

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

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

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