7

我似乎没有找到如何获得SynchronizationContext给定的Thread

Thread uiThread = UIConfiguration.UIThread;
SynchronizationContext context = uiThread.Huh?;

我为什么需要那个?

因为我需要从整个前端应用程序的不同位置发布到 UIThread。所以我在一个名为UIConfiguration. 我在方法中设置了这个属性Program.Main

UIConfiguration.UIThread = Thread.CurrentThread;

在那一刻,我可以确定我有正确的线程,但是我不能设置像这样的静态属性

UIConfiguration.SynchronizationContext = SynchronizationContext.Current

因为尚未安装该类的 WinForms 实现。由于每个线程都有自己的 SynchronizationContext,因此必须可以从给定Thread对象中检索它,还是我完全错了?

4

5 回答 5

14

这是不可能的。问题是 aSynchronizationContext和 aThread实际上是两个完全独立的概念。

虽然确实 Windows 窗体和 WPF 都SynchronizationContext为主线程设置了 a,但大多数其他线程都没有。例如,ThreadPool 中的所有线程都不包含它们自己的 SynchronizationContext(当然,除非您安装自己的)。

a 也可能SynchronizationContextthreads 和 threading 完全无关。可以轻松设置同步上下文以同步到外部服务或整个线程池等。

在您的情况下,我建议您UIConfiguration.SynchronizationContext在初始主表单的 Loaded 事件中设置您。上下文保证在该点启动,并且在任何情况下都将在消息泵启动之前不可用。

于 2010-11-05T15:59:12.640 回答
7

我知道这是一个老问题,并为死灵道歉,但我刚刚找到了一个解决这个问题的方法,我认为这可能对我们这些一直在谷歌搜索的人有用(并且它不需要 Control 实例)。

基本上,您可以创建一个 WindowsFormsSynchronizationContext 的实例并在您的Main函数中手动设置上下文,如下所示:

    _UISyncContext = new WindowsFormsSynchronizationContext();
    SynchronizationContext.SetSynchronizationContext(_UISyncContext);

我已经在我的应用程序中做到了这一点,它可以完美地运行而没有问题。但是,我应该指出,我Main的标记为 STAThread,所以我不确定如果你Main的标记为 MTAThread,这是否仍然有效(或者是否有必要)。

编辑:我忘了提及它,但_UISyncContext已经Program在我的应用程序类的模块级别定义。

于 2011-04-17T12:11:42.407 回答
7

我发现 Alex Davies 的书“Async in C# 5.0. O'Reilly Publ., 2012”, p.48-49 中的以下段落对我最简洁和最有帮助:

  • " SynchronizationContext 是 .NET Framework 提供的一个类,它具有在特定类型的线程中运行代码的能力
    。.NET 使用的同步上下文有多种,其中最重要的是 WinForms 使用的 UI 线程上下文和WPF。”

  • “实例SynchronizationContext本身并没有做任何非常有用的事情,因此它的所有实际实例往往都是子类。

    它还具有静态成员,可让您读取和控制当前的SynchronizationContext.

    currentSynchronizationContext是当前线程的一个属性。

    这个想法是,在您在特殊线程中运行的任何时候,您都应该能够获取当前SynchronizationContext并存储它。稍后,您可以使用它在您开始的特殊线程上运行代码。这一切都应该是可能的,不需要确切知道你从哪个线程开始,只要你可以使用 SynchronizationContext,你就可以回到它

    SynchronizationContext 的重要方法是Post,它可以使委托在正确的上下文中运行”

  • 一些 SynchronizationContexts 封装了单个线程,例如 UI 线程
    一些封装了特定类型的线程- 例如,线程池 - 但可以选择其中任何一个线程来发布委托。有些实际上并没有改变哪个线程代码运行,但仅用于监视,如 ASP.NET 同步上下文"

于 2013-04-29T02:11:46.263 回答
2

我不相信每个线程都有自己的SynchronizationContext- 它只有一个 thread-local SynchronizationContext

你为什么不直接设置你UIConfiguration.UIThreadLoaded表单,或者类似的东西?

于 2010-11-05T15:48:22.623 回答
1

完整且有效的扩展方法,用于SynchronizationContext从 aThreadExecutionContext(或null如果不存在)或DispatcherSynchronizationContext从 a 获取 a Dispatcher在.NET 4.6.2上测试。

using Ectx = ExecutionContext;
using Sctx = SynchronizationContext;
using Dctx = DispatcherSynchronizationContext;

public static class _ext
{
    // DispatcherSynchronizationContext from Dispatcher
    public static Dctx GetSyncCtx(this Dispatcher d) => d?.Thread.GetSyncCtx() as Dctx;

    // SynchronizationContext from Thread
    public static Sctx GetSyncCtx(this Thread th) => th?.ExecutionContext?.GetSyncCtx();

    // SynchronizationContext from ExecutionContext
    public static Sctx GetSyncCtx(this Ectx x) => __get(x);

    /* ... continued below ... */
}

所有上述函数最终都会调用__get下面显示的代码,这需要一些解释。

请注意,这__get是一个静态字段,使用可丢弃的 lambda 块预先初始化。这允许我们巧妙地拦截第一个调用者以便运行一次性初始化,它准备了一个更快且无反射的微小且永久的替换委托。

无畏初始化工作的最后一步是将替换替换为“__get”,这同时又可悲地意味着代码丢弃了自己,不留下任何痕迹,所有后续调用者都直接进入DynamicMethod正确的地方,甚至没有任何绕过逻辑的提示。

static Func<Ectx, Sctx> __get = arg =>
{
    // Hijack the first caller to do initialization...

    var fi = typeof(Ectx).GetField(
        "_syncContext",                         // private field in 'ExecutionContext'
        BindingFlags.NonPublic|BindingFlags.Instance);

    var dm = new DynamicMethod(
        "foo",                                  // (any name)
        typeof(Sctx),                           // getter return type
        new[] { typeof(Ectx) },                 // type of getter's single arg
        typeof(Ectx),                           // "owner" type
        true);                                  // allow private field access

    var il = dm.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldfld, fi);
    il.Emit(OpCodes.Ret);

    // ...now replace ourself...
    __get = (Func<Ectx, Sctx>)dm.CreateDelegate(typeof(Func<Ectx, Sctx>));

    // oh yeah, don't forget to handle the first caller's request
    return __get(arg);                 //  ...never to come back here again. SAD!
};

可爱的部分是最后——为了真正为抢先的第一个调用者获取值——函数表面上用自己的参数调用自己,但通过立即替换自己来避免递归。

SynchronizationContext在本页讨论的特定问题上展示这种不寻常的技术并没有特别的理由。使用传统反射(加上一些扩展方法结霜)可以轻松轻松地解决从_syncContext领域中获取的问题。ExecutionContext但我想我会分享这种我个人使用了很长时间的方法,因为它也很容易适应并且同样广泛适用于此类情况。

当访问非公共领域需要极端性能时,它尤其合适。我想我最初在基于 QPC 的频率计数器中使用了它,在该频率计数器中,场是在每 20 或 25 纳秒迭代一次的紧密循环中读取的,这对于传统反射来说实际上是不可能的。

这结束了主要答案,但下面我包含了一些有趣的点,与提问者的询问不太相关,与刚刚展示的技术更相关。

 


运行时调用者

为清楚起见,我在上面显示的代码中将“安装交换”和“第一次使用”步骤分成两行,而不是我自己的代码中的内容(与以前的版本相比,以下版本还避免了一次主内存提取,可能涉及线程安全,请参阅下面的详细讨论):

return (__get = (Func<Ectx, Sctx>)dm.CreateDel...(...))(arg);

换句话说,所有调用者,包括 first,都以完全相同的方式获取值,并且没有使用反射代码来这样做。它只写替换getter。感谢il-visualizerDynamicMethod ,我们可以在运行时在调试器中看到它的主体:

ldarg.0<br>ldfld SynchronizationContext _syncContext/ExecutionContext<br>ret

无锁线程安全

我应该注意到,考虑到 .NET内存模型和无锁理念,函数体中的交换是完全线程安全的操作。后者有利于前进保证,但可能会以重复或冗余工作为代价。在完全合理的理论基础上正确允许进行多路竞赛初始化:

  • 竞赛入口点(初始化代码)是全局预配置和保护的(由 .NET 加载程序),因此(多个)竞赛者(如果有)输入相同的初始化程序,这永远不会被视为null.
  • 多个竞赛产品(吸气剂)在逻辑上总是相同的,因此任何特定的竞赛者(或后来的非竞赛调用者)碰巧拿起哪一个,甚至是否有任何竞赛者最终使用他们自己生产的那个都无关紧要;
  • 每个安装交换都是一个单独的 size 存储,IntPtr对于任何相应的平台位数保证是原子的;
  • 最后,对于完美的形式正确性而言,技术上绝对至关重要,“失败者”的工作产品被回收GC,因此不会泄漏。在这种类型的比赛中,除了最后一名完成者之外的所有参赛者都是失败者(因为其他所有人的努力都会被相同的结果轻松而简单地覆盖)。

尽管我相信这些点结合起来可以在所有可能的情况下完全保护编写的代码,但如果您仍然对总体结论持怀疑态度或警惕,您可以随时添加额外的防弹层:

var tmp = (Func<Ectx, Sctx>)dm.CreateDelegate(typeof(Func<Ectx, Sctx>));
Thread.MemoryBarrier();
__get = tmp;
return tmp(arg);

这只是一个偏执的版本。与较早的浓缩单线一样,.NET 内存模型保证只有一个存储- 和零提取-到 '__get' 的位置。(顶部的完整扩展示例确实进行了额外的主内存提取,但由于第二个要点仍然是合理的)正如我所提到的,这些都不是正确性所必需的,但理论上它可以提供微不足道的性能奖励:通过提前结束比赛,激进的刷新可以在极少数情况下防止脏缓存行上的后续调用者不必要地(但同样是无害的)比赛。

双重思考

对最终的超快速方法的调用仍然通过前面显示的静态扩展方法来实现。这是因为我们还需要以某种方式表示在编译时实际存在的入口点,以便编译器绑定和传播元数据。对于 IDE 中强类型元数据和智能感知的压倒性便利性,对于直到运行时才能真正解析的自定义代码,双重重击是一个很小的代价。然而,它的运行速度至少与静态编译的代码一样快,每次调用都进行大量反射要快得多,因此我们可以两全其美!

于 2017-07-08T12:59:54.127 回答