157

在 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;在方法中,有什么区别?

4

8 回答 8

200

SynchronizationContext 有什么作用?

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

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

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

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

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

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

这就是为什么您必须在特定分配之前以某种方式从工作线程“切换回”到“UI 线程”(在哪里myTextBox创建)。这是按如下方式完成的:

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

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


最后的评论和提示:

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

    所以请记住这条适用于 Windows 窗体的简单规则:不要从创建控件或窗体的线程之外的线程访问控件或窗体。如果您必须这样做,请使用上述SynchronizationContext机制,或者Control.BeginInvoke(这是一种特定于 Windows 窗体的执行完全相同操作的方式)。

  • 如果您正在针对 .NET 4.5 或更高版本进行编程,您可以通过将显式使用SynchronizationContext, ThreadPool.QueueUserWorkItem,control.BeginInvoke等的代码转换为新的async/await关键字任务并行库 (TPL)来简化您的工作,即围绕 API和TaskTask<TResult>。这些将在很大程度上负责捕获 UI 线程的同步上下文,启动异步操作,然后返回 UI 线程,以便您可以处理操作的结果。

于 2013-08-07T08:37:05.007 回答
26

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

如果您熟悉 Win32 编程模型,那么PostMessageSendMessageAPI 非常相似,您可以调用 API 来从与目标窗口不同的线程分派消息。

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

于 2013-08-07T09:11:24.027 回答
20

它存储同步提供程序,一个派生自 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 中使用,它仍然有效,它在库中很重要。这当然不是这样的代码的一个很好的例子,你总是知道你在这里有什么样的文本框,所以你总是知道是使用 Control.BeginInvoke 还是 Dispatcher.BeginInvoke。实际上使用 SynchronizationContext.Current 并不常见。

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

于 2013-08-07T08:16:51.723 回答
5

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

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

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

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

于 2013-08-07T08:04:37.637 回答
4

到源头

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

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

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

于 2017-01-31T15:38:14.660 回答
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)是传递给线程的方法,因此它应该休眠该线程而不是主 UI 线程,不!即使 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 方法,您就可以了放。

希望这对你有帮助,伙计!

于 2018-08-30T08:41:08.003 回答
3

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

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

每个.Net应用程序,除了控制台应用程序,都有基于特定底层框架的该类的特定实现,即:WPF、WindowsForm、Asp Net、Silverlight等。

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

而“上下文”这个词代表执行上下文,即等待代码将被执行的当前执行上下文,即异步代码与其等待代码之间的同步发生在特定的执行上下文中,因此这个对象被命名为SynchronizationContext:它表示将关注异步代码同步和等待代码执行的执行上下文

于 2019-03-23T17:10:57.690 回答
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());
}
于 2018-02-20T23:46:10.170 回答