从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 中强类型元数据和智能感知的压倒性便利性,对于直到运行时才能真正解析的自定义代码,双重重击是一个很小的代价。然而,它的运行速度至少与静态编译的代码一样快,方式比每次调用都进行大量反射要快,因此我们可以两全其美!