SynchronizationContext 有啥作用?

Posted

技术标签:

【中文标题】SynchronizationContext 有啥作用?【英文标题】:What does SynchronizationContext do?SynchronizationContext 有什么作用? 【发布时间】:2013-08-08 11:31:50 【问题描述】:

在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);
);

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

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

【问题讨论】:

The fine manual has this to say 这个类实现的同步模型的目的是让公共语言运行库的内部异步/同步操作在不同的同步模型下都能正常运行。此模型还简化了托管应用程序必须遵循的一些要求,以便在不同的同步环境下正常工作。 恕我直言异步等待已经这样做了 @RoyiNamir:是的,但你猜怎么着:async/await 依赖于下面的SynchronizationContext Stephen Toub 的这篇文章很棒,Await, SynchronizationContext, and Console Apps 【参考方案1】:

SynchronizationContext 有什么作用?

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

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

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

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

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

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

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

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

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


最后的备注和提示:

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

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

如果您正在针对 .NET 4.5 或更高版本进行编程,您可以通过将明确使用 SynchronizationContextThreadPool.QueueUserWorkItemcontrol.BeginInvoke 等的代码转换为新的 async / await keywords 来简化您的工作和Task Parallel Library (TPL),即围绕TaskTask<TResult> 类的API。这些将在很大程度上负责捕获 UI 线程的同步上下文,启动异步操作,然后返回到 UI 线程,以便您可以处理操作的结果。

【讨论】:

你说 Windows 窗体,像许多其他 UI 框架一样,只允许在同一线程上操作控件,但 Windows 中的所有窗口必须由创建它的同一线程访问. @user34660:不,这是不正确的。您可以拥有多个创建 Windows 窗体控件的线程。但是每个控件都与创建它的一个线程相关联,并且只能由该一个线程访问。来自不同 UI 线程的控件在它们之间的交互方式上也非常有限:一个不能是另一个的父/子,它们之间的数据绑定是不可能的,等等。最后,创建控件的每个线程都需要自己的消息循环(由Application.Run, IIRC 开始)。这是一个相当高级的话题,不是随便做的。 我的第一条评论是因为你说“像许多其他 UI 框架一样”暗示 一些 窗口 允许 从一个 不同的 线程,但没有 Windows 窗口。您不能为 同一个窗口“拥有多个创建 Windows 窗体控件的线程”,并且“必须由同一个线程访问”和“只能由该线程访问”是在说同样的事情。我怀疑是否可以为同一个窗口创建“来自不同 UI 线程的控件”。对于我们这些在 .Net 之前有 Windows 编程经验的人来说,所有这些都不是高级的。 所有这些关于“windows”和“windows windows”的讨论都让我头晕目眩。我有没有提到这些“窗户”?我不这么认为...... @ibubi:我不确定我是否理解你的问题。任何线程的同步上下文要么未设置(null),要么是SynchronizationContext 的实例(或其子类)。这句话的重点不是你得到什么,而是你不会得到什么:UI 线程的同步上下文。【参考方案2】:

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

如果您熟悉 Win32 编程模型,一个非常相似的类比是 PostMessageSendMessage API,您可以调用它们来从与目标窗口线程不同的线程分派消息。

这里很好地解释了同步上下文是什么: It's All About the SynchronizationContext.

【讨论】:

【参考方案3】:

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

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

注意这个 sn-p 有点危险,它只有在你从 UI 线程调用它时才能正常工作。 SynchronizationContext.Current 在不同的线程中具有不同的值。只有 UI 线程具有可用值。这就是代码必须复制它的原因。一种更易读、更安全的方法,在 Winforms 应用程序中:

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

它的优点是它在从 any 线程调用时工作。使用 SynchronizationContext.Current 的优点是,无论代码用于 Winforms 还是 WPF,它仍然有效,它在库中很重要。这肯定不是这样的代码的一个很好的例子,你总是知道你在这里有什么样的文本框,所以你总是知道是使用 Control.BeginInvoke 还是 Dispatcher.BeginInvoke。实际上使用 SynchronizationContext.Current 并不常见。

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

【讨论】:

对不起,为什么让UI线程句柄是线程安全的?即我认为当 Post() 触发时 UI 线程可能正在使用 myTextBox,这安全吗? 你的英语很难解码。您原来的 sn-p 只有在从 UI 线程调用时才能正常工作。这是一个非常常见的情况。只有这样它才会回传到 UI 线程。如果从工作线程调用它,则 Post() 委托目标将在线程池线程上运行。卡布姆。这是你想自己尝试的东西。启动一个线程,让线程调用这段代码。如果代码因 NullReferenceException 而崩溃,您就做对了。 “您必须从创建 TextBox 的线程更新 TextBox.Text 属性” - 他们为什么要这样设计?【参考方案4】:

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

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

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

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

【讨论】:

“是的,你显示的代码基本没用。为什么要创建一个后台线程,只需要立即回到主UI线程?这只是一个例子。” - 如果文件很大,从文件中读取可能是一项很长的任务,这可能会阻塞 UI 线程并使其无响应 我有一个愚蠢的问题。每个线程都有一个 ID,我想 UI 线程也有一个 ID= 2 例如。然后,当我在线程池线程上时,我可以这样做: var thread = GetThread(2); thread.Execute(() => textbox1.Text = "foo") ? @John - 不,我认为这不起作用,因为线程已经在执行。您不能执行已经执行的线程。仅在线程未运行时执行(IIRC)【参考方案5】:

To the Source

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

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

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

【讨论】:

同步上下文不一定与特定线程相关联。多个线程可能会处理对单个同步上下文的请求,而单个线程可能会处理对多个同步上下文的请求。 @Servy 他写道:“请注意,每个线程都可以有自己的 SynchronizatonContext。”这是真的,不是吗? “可以”的意思是“有可能”。 @DavidKlempfner 问题不在于答案中的引用,而在于作者对此的评论。你引用的那一点不是问题。这是作者的声明,如果您从线程中获取同步上下文,则意味着它将在该线程上执行发布的委托。答案引用的那一点只是说一个线程可能有一个上下文,而没有评论发布到它时实际发生的事情。【参考方案6】:

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 工作时阻塞 UI。

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

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

【讨论】:

【参考方案7】:

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

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

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

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

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

【讨论】:

【参考方案8】:

此示例来自 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());

【讨论】:

以上是关于SynchronizationContext 有啥作用?的主要内容,如果未能解决你的问题,请参考以下文章

从给定线程获取 SynchronizationContext

为啥控制台应用程序中没有捕获默认的 SynchronizationContext?

寻找自定义 SynchronizationContext 的示例(单元测试所需)

SynchronizationContext是什么?

SynchronizationContext笔记

SynchronizationContext.Current 在不同的应用程序域上运行时为空