完整且有效的扩展方法,用于SynchronizationContext
从 aThread
或ExecutionContext
(或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
,我们可以在运行时在调试器中看到它的主体:
无锁线程安全
我应该注意到,考虑到 .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 中强类型元数据和智能感知的压倒性便利性,对于直到运行时才能真正解析的自定义代码,双重重击是一个很小的代价。然而,它的运行速度至少与静态编译的代码一样快,比每次调用都进行大量反射要快得多,因此我们可以两全其美!