SynchronizationContext 做什么?

在《编程 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;,有什么区别?

68781 次浏览

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

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

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

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

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

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

请注意,这个代码片段有点危险,它只有在您从 UI 线程调用它时才能正常工作。同步上下文。Current 在不同的线程中有不同的值。只有 UI 线程有可用的值。这就是代码必须复制它的原因。在 Winforms 应用程序中,这是一种更易读、更安全的方法:

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

它的优点是当从 任何线程调用时可以正常工作。使用 SynchronizationContext 的优点。当前的问题是,无论代码是在 Winform 还是 WPF 中使用,它都能正常工作,这对于库来说很重要。这当然是 没有这样的代码的一个很好的例子,你总是知道你在这里有什么样的文本框,所以你总是知道是否使用控件。BeginInvoke 或 Dispatcher。BeginInvoke.实际使用 SynchronizationContext。电流并不常见。

这本书试图教给你关于线程的知识,所以使用这个有缺陷的例子是可以的。在现实生活中,在少数情况下,也许吧考虑使用 SynchronizationContext。当前,您仍然可以将它留给 C # 的异步/等待关键字或任务计划程序。FromCurrentSynchronizationContext ()为您完成此操作。但是请注意,出于完全相同的原因,当您在错误的线程上使用它们时,它们仍然会像代码片段那样行为不当。这里有一个非常常见的问题,额外的抽象级别是有用的,但是它使得我们很难找出它们为什么不能正确工作。希望这本书也能告诉你什么时候不要使用它:)

SynchronizationContext 做什么?

简单地说,SynchronizationContext表示可能执行代码的位置。然后将在该位置调用传递给其 SendPost方法的委托。(PostSend的非阻塞/异步版本。)

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

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

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

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

传递给 ThreadPool.QueueUserWorkItem的代码将在线程池辅助线程上运行。也就是说,它不会在创建 myTextBox的线程上执行,所以 Windows 窗体迟早会抛出异常(特别是在发布版本中) ,告诉您可能无法从另一个线程访问 myTextBox

这就是为什么你必须以某种方式“切换回”从工作线程到“ UI 线程”(在那里 myTextBox被创建)之前,特定的任务。这方面的工作如下:

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

  2. 无论何时需要操作 UI 元素(但是不在或者可能不在 UI 线程上) ,通过 originalContext访问 Windows 窗体的同步上下文,并将操作 UI 的代码传递给 SendPost


最后的评论和提示:

  • 同步上下文 不会为您做的就是告诉您哪些代码必须在特定的位置/上下文中运行,以及哪些代码可以正常执行,而不需要将其传递给 SynchronizationContext。为了确定这一点,您必须了解所编程的框架的规则和要求ーー在本例中是 Windows 窗体。

    因此,请记住 Windows 窗体的这个简单规则: 不要从创建控件或窗体的线程以外的线程访问控件或窗体。如果必须这样做,可以使用上面描述的 SynchronizationContext机制,或者使用 Control.BeginInvoke(这是一种特定于 Windows 窗体的完全相同的方法)。

  • 如果你的程序。NET 4.5或更高版本,您可以通过将显式使用 SynchronizationContextThreadPool.QueueUserWorkItemcontrol.BeginInvoke等的代码转换为新的 ABC3/await关键字任务并行库(TPL),即围绕 TaskThreadPool.QueueUserWorkItem0类的 API,从而使您的生活变得更加轻松。在很大程度上,它们将负责捕获 UI 线程的同步上下文,启动异步操作,然后返回到 UI 线程,以便您可以处理操作的结果。

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

如果您熟悉 Win32编程模型,那么非常类似于 PostMessageSendMessage API,您可以调用它们从不同于目标窗口的线程发送消息。

下面是对什么是同步上下文的一个很好的解释: It’s All About the SynchronizationContext

找到源头

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

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

例如: 假设您有两个线程,Thread1和 Thread2。例如,Thread1正在执行一些工作,然后 Thread1希望在 Thread2上执行代码。一种可能的方法是向 Thread2请求其 SynchronizationContext 对象,将其给 Thread1,然后 Thread1可以调用 SynchronizationContext。发送以执行 Thread2上的代码。

这个例子来自 Joseph Albahari 的 Linqpad 示例,但它确实有助于理解 Synchronization 上下文的作用。

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());
}

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;
}

同步上下文。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;
}

最后一个例子和这个例子执行相同的操作。

最后,将 SynchronizationContext 看作一个线程。它不是一个线程,它定义了一个线程(注意,并非所有线程都有 SyncContext)。每当我们调用 Post 或 Send 方法来更新 UI 时,就像通常从主 UI 线程更新 UI 一样。如果出于某些原因,您需要从另一个线程更新 UI,请确保该线程具有主 UI 线程的 SyncContext,并且只需调用发送或发布方法,该方法是您想要执行的,并且您已经设置好了。

希望这个能帮到你,伙计!

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

从技术角度来看,SC 是一个简单的 C # 类,面向支持并提供其功能,特别是为任务并行库对象。

每一个。除了控制台应用程序外,Net 应用程序基于特定的底层框架(例如: WPF、 WindowsForm、 Asp Net、 Silverlight 等)对该类进行了定制实现。

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

“上下文”代表执行上下文。也就是说,等待代码将在当前执行上下文中执行——即异步代码与其等待代码之间的同步发生在特定的执行上下文中。因此,此对象被命名为 SynchronizationContext。

它表示负责异步代码同步和等待代码执行的执行上下文。