从给定线程获取 SynchronizationContext
Posted
技术标签:
【中文标题】从给定线程获取 SynchronizationContext【英文标题】:Get SynchronizationContext from a given Thread 【发布时间】:2011-05-05 16:24:18 【问题描述】:我似乎没有找到如何获取给定Thread
的SynchronizationContext
:
Thread uiThread = UIConfiguration.UIThread;
SynchronizationContext context = uiThread.Huh?;
我为什么需要那个?
因为我需要从整个前端应用程序的不同位置发布到 UIThread。所以我在一个名为UIConfiguration
的类中定义了一个静态属性。我在Program.Main
方法中设置了这个属性:
UIConfiguration.UIThread = Thread.CurrentThread;
在那一刻,我可以确定我有正确的线程,但是我不能设置像这样的静态属性
UIConfiguration.SynchronizationContext = SynchronizationContext.Current
因为尚未安装该类的 WinForms 实现。由于每个线程都有自己的 SynchronizationContext,因此必须可以从给定的 Thread
对象中检索它,还是我完全错了?
【问题讨论】:
稍后(在加载 WinForms 实现之后),您可以像这样获取 UI 线程的同步上下文:(未测试)var context = (SynchronizationContext)someUiControl.Invoke(new Func<SynchronizationContext>(() => SynchronizationContext.Current));
并将其缓存以供以后使用。
@Heinzi:看起来很有创意。但是,我需要一个控件,这比需要 SynchronizationContext 对象还要糟糕。
不知道为什么,但我从我的"Is the phrase from a book “The current SynchronizationContext is a property of the current thread” correct"?" 到这个问题的链接没有出现在右侧边栏的相关或链接部分中,所以我把它放在这个评论中......所以,它后来出现了
【参考方案1】:
这是不可能的。问题是SynchronizationContext
和Thread
实际上是两个完全不同的概念。
虽然 Windows 窗体和 WPF 确实都为主线程设置了 SynchronizationContext
,但大多数其他线程都没有。例如,ThreadPool 中的所有线程都不包含它们自己的 SynchronizationContext(当然,除非您安装自己的)。
SynchronizationContext
也有可能与线程和线程完全无关。可以轻松设置同步上下文以同步到外部服务或整个线程池等。
在您的情况下,我建议您在初始主表单的 Loaded 事件中设置您的 UIConfiguration.SynchronizationContext
。上下文保证在该点启动,并且在任何情况下都将在消息泵启动之前不可用。
【讨论】:
关于缺少的 someThread.SynchronizationContext 属性,如果 Thread 对象提供该属性,仍然会受到赞赏。它的概念是“如果我是那个特定的线程,SynchronizationContext.Current
会给我什么?对于没有上下文的线程,它将返回 null
,就像 SynchronizationContext.Current 一样。
除此之外,感谢您提供的信息丰富的答案。就像 Jon 和你建议我把它放在 Load
事件处理程序中。
Reed,我问了子问题"Is the phrase from a book “The current SynchronizationContext is a property of the current thread” correct"?"【参考方案2】:
我知道这是一个老问题,并为死灵道歉,但我刚刚找到了一个解决这个问题的方法,我认为它可能对我们这些一直在谷歌搜索的人有用(而且它不需要控制实例)。
基本上,您可以创建一个 WindowsFormsSynchronizationContext 实例并在 Main
函数中手动设置上下文,如下所示:
_UISyncContext = new WindowsFormsSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(_UISyncContext);
我已经在我的应用程序中做到了这一点,并且它可以完美运行,没有任何问题。但是,我应该指出,我的 Main
被标记为 STAThread,所以如果你的 Main
被标记为 MTAThread,我不确定这是否仍然有效(或者是否有必要)。
编辑:我忘了提及它,但 _UISyncContext
已经在我的应用程序的 Program
类的模块级别定义。
【讨论】:
【参考方案3】:我发现 Alex Davies 的书“Async in C# 5.0. O'Reilly Publ., 2012”, p.48-49 中的以下段落对我最简洁和有帮助:
"SynchronizationContext is a class provided by the .NET Framework,能够在特定类型的线程中运行代码。 .NET 使用了多种同步上下文,其中最重要的是 WinForms 和 WPF 使用的 UI 线程上下文。”
“SynchronizationContext
的实例本身并没有做任何非常有用的事情,所以它的所有实际实例都倾向于是子类。
它还有静态成员,可以让你读取和控制当前的SynchronizationContext
。
当前SynchronizationContext
是当前线程的一个属性。
这个想法是,在您运行在一个特殊线程中的任何时候,您都应该能够获取当前的SynchronizationContext
并存储它。稍后,您可以使用它在您开始的特殊线程上运行代码。所有这一切都应该是可能的不需要确切知道你从哪个线程开始,只要你可以使用 SynchronizationContext,你就可以回到它。
SynchronizationContext 的重要方法是Post
,它可以使委托在正确的上下文中运行"
.
“Some SynchronizationContexts encapsulate a single thread, like the UI thread. 有些封装了一种特定类型的线程——例如,线程池——但可以选择这些线程中的任何一个将委托发布到。有些实际上并没有改变代码运行在哪个线程上,而只是用于监控,比如 ASP.NET 同步上下文”
【讨论】:
+1 谢谢。虽然它没有直接回答我原来的问题,但您的回答仍然增加了一些有价值的信息。【参考方案4】:我不相信每个线程确实都有自己的SynchronizationContext
- 它只是有一个线程本地SynchronizationContext
。
您为什么不在表单的Loaded
事件中设置UIConfiguration.UIThread
或类似的东西?
【讨论】:
这样做,对“在代码中间”的那行代码并不完全满意 - 我使用 Application.Run() 两次,首先是启动屏幕,然后是主屏幕形式,所以它现在被放置在初始屏幕的加载事件中 - 但实际上:只要它在那里,它就可以工作。如果闪屏碰巧被删除,我会立即注意到它,因为该属性会引发NullPointerException
。
乔恩,我已经问过后续"Is the phrase from a book “The current SynchronizationContext is a property of the current thread” correct"?"【参考方案5】:
从Thread
或ExecutionContext
(或null
,如果不存在)或DispatcherSynchronizationContext
从Dispatcher
获取SynchronizationContext
的完整且有效的扩展方法。在 .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
的特定问题上演示这种不寻常的技术并没有特别的理由。从ExecutionContext
中获取_syncContext
字段可以通过传统反射轻松轻松地解决(加上一些扩展方法结霜)。但我想我会分享这种我个人使用了很长时间的方法,因为它也很容易适应并且同样广泛适用于此类情况。
当访问非公共字段需要极端性能时,它尤其合适。我想我最初在基于 QPC 的频率计数器中使用了它,在该频率计数器中,场是在每 20 或 25 纳秒迭代一次的紧密循环中读取的,这对于传统反射来说实际上是不可能的。
主要答案到此结束,但下面我包含了一些有趣的点,与提问者的询问不太相关,与刚刚演示的技术更相关。
运行时调用者
为了清楚起见,我在上面显示的代码中将“安装交换”和“第一次使用”步骤分成两行,而不是我自己的代码中的(以下版本也避免了一次主内存提取与之前的相比,可能涉及线程安全,请参阅下面的详细讨论):
return (__get = (Func<Ectx, Sctx>)dm.CreateDel...(...))(arg);
换句话说,所有调用者,包括第一个,都以完全相同的方式获取值,并且从来没有使用反射代码来这样做。它只写替换getter。感谢il-visualizer,我们可以在运行时在调试器中看到DynamicMethod
的主体:
无锁线程安全
我应该注意到,鉴于 .NET memory model 和无锁理念,函数体中的交换是完全线程安全的操作。后者倾向于以重复或冗余工作为代价的前进保证。在完全合理的理论基础上,正确允许初始化多路竞赛:
竞赛入口点(初始化代码)是全局预配置和保护的(由 .NET 加载程序),因此(多个)竞赛者(如果有)输入相同的初始化程序,永远不会被视为null
。
多个竞赛产品(getter)在逻辑上总是相同的,因此任何特定竞赛者(或后来的非竞赛调用者)碰巧拿起哪一个,甚至是否有任何竞赛者最终使用他们的那个都无关紧要自己生产;
每个安装交换都是一个大小为IntPtr
的单一存储,对于任何相应的平台位数保证是原子的;
最后,在技术上对完美的形式正确性绝对至关重要,“失败者”的工作产品由GC
回收,因此不会泄漏。在this type of race 中,除了最后一个完赛者之外的所有参赛者都是失败者(因为其他所有人的努力都会被相同的结果轻松而简单地覆盖)。
尽管我相信这些点结合起来可以在所有可能的情况下完全保护编写的代码,但如果您仍然对总体结论持怀疑态度或警惕,您可以随时添加额外的防弹层:
var tmp = (Func<Ectx, Sctx>)dm.CreateDelegate(typeof(Func<Ectx, Sctx>));
Thread.MemoryBarrier();
__get = tmp;
return tmp(arg);
这只是一个偏执的版本。与较早的浓缩单线一样,.NET 内存模型保证只有一个 store——和零个 fetches——到 '__get' 的位置。 (顶部的完整扩展示例确实进行了额外的主内存提取,但由于第二个要点仍然是合理的)正如我所提到的,这些都不是正确性所必需的,但理论上它可以提供微不足道的性能奖励:通过提前结束比赛,激进的刷新可以在极少数情况下防止脏缓存行上的后续调用者不必要地(但同样是无害地)比赛。
双重思考
通过前面显示的静态扩展方法调用最终的超快速方法仍然是thunked。 这是因为我们还需要以某种方式表示在编译时实际存在的入口点,以便编译器绑定和传播元数据。对于 IDE 中强类型元数据和智能感知的压倒性便利性,对于直到运行时才能真正解析的自定义代码,双重重击是一个很小的代价。然而,它的运行速度至少与静态编译的代码一样快,方式比每次调用都进行大量反射要快,因此我们可以两全其美!
【讨论】:
你基本上是说一个线程有一个执行上下文,它有一个私有字段,它是一个同步上下文,它可以通过反射访问。事实证明这是真的。但是,此同步上下文调用回您从中检索它的线程是不正确的,这是它需要具有的属性。因此,您的答案不仅很难破译,而且也是错误的。抱歉听起来有点粗鲁,但我想澄清我的反对意见。 @briantyler 该代码的工作方式与广告宣传的一样,如果调用者指定的Thread
有SynchronizationContext
(如果它有一个(并且在WPF 上至关重要,如果没有,则不会默默地要求一个) t,这是这里真正的用途)。如果调用者指定Thread.CurrentThread
,确实操作有些空洞。
重点是 Glen,您获得了一个 SynchronizationContext
,但实际上您并没有为调用者指定的线程获得一个。该上下文不会调用到指定的线程,这是您希望它执行的操作,因此它不是无用的。以上是关于从给定线程获取 SynchronizationContext的主要内容,如果未能解决你的问题,请参考以下文章