から(または存在しない場合)、またはからSynchronizationContext
を取得するための完全で機能する拡張メソッド。.NET4.6.2でテスト済み。Thread
ExecutionContext
null
DispatcherSynchronizationContext
Dispatcher
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
は静的フィールドであり、破棄可能なラムダブロックで事前に初期化されていることに注意してください。これにより、1回限りの初期化を実行するために、最初の呼び出し元のみを適切にインターセプトできます。これにより、はるかに高速でリフレクションのない、小さくて永続的な置換デリゲートが準備されます。
勇敢な初期化作業の最後の行動は、置換を'__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ナノ秒ごとに繰り返されるタイトなループで読み取られました。これは、従来の反射では実際には不可能でした。
これで主な答えは終わりですが、以下に、質問者の質問とはあまり関係のない興味深い点をいくつか紹介しました。
ランタイム発信者
わかりやすくするために、上記のコードでは、「インストールスワップ」と「最初の使用」の手順を、自分のコードにあるものとは対照的に、2つの別々の行に分けました(次のバージョンでも、前のバージョンと比較して1つのメインメモリフェッチが回避されます) 、スレッドセーフに関係している可能性があります。以下の詳細な説明を参照してください):
return (__get = (Func<Ectx, Sctx>)dm.CreateDel...(...))(arg);
つまり、最初の呼び出し元を含むすべての呼び出し元がまったく同じ方法で値をフェッチし、そのためにリフレクションコードが使用されることはありません。置換ゲッターのみを書き込みます。il-visualizerの好意によりDynamicMethod
、実行時にデバッガーでその本体を確認できます。

ロックフリースレッドセーフ
関数本体でのスワッピングは、.NETメモリモデルとロックフリーの哲学を考えると、完全にスレッドセーフな操作であることに注意してください。後者は、重複または冗長な作業を行うことを犠牲にして、前向きな保証を支持します。初期化するマルチウェイレースは、完全に健全な理論的根拠に基づいて正しく許可されています。
- レースエントリポイント(初期化コード)はグローバルに事前構成され、(.NETローダーによって)保護されているため、(複数の)レーサー(存在する場合)が同じ初期化子に入ることができます
null
。
- 複数のレース製品(ゲッター)は常に論理的に同一であるため、特定のレーサー(または後で非レースの発信者)がたまたまどのレーサーをピックアップするか、またはレーサーが自分で作成したものを使用するかどうかは関係ありません。
- 各インストールスワップはサイズの単一ストアであり
IntPtr
、それぞれのプラットフォームのビット数に対してアトミックであることが保証されています。
- 最後に、正式な正しさを完全にするために技術的に絶対的に重要なのは、「敗者」の作業成果物が回収される
GC
ため、リークが発生しないことです。このタイプのレースでは、敗者は最後のフィニッシャーを除くすべてのレーサーです(他のすべての人の努力は、同じ結果で簡単にそして要約的に上書きされるため)。
これらの点が組み合わさって、考えられるあらゆる状況下で記述されたコードを完全に保護すると思いますが、それでも全体的な結論に疑問がある場合や警戒している場合は、いつでも防弾の層を追加できます。
var tmp = (Func<Ectx, Sctx>)dm.CreateDelegate(typeof(Func<Ectx, Sctx>));
Thread.MemoryBarrier();
__get = tmp;
return tmp(arg);
単なるパラニオドバージョンです。以前の凝縮されたワンライナーと同様に、.NETメモリモデルは、'__get'の場所に正確に1つのストアがあり、フェッチがゼロであることを保証します。(上部の完全に拡張された例は、追加のメインメモリフェッチを実行しますが、2番目の箇条書きのおかげでまだ健全です)前述したように、これは正確さのために必要ではありませんが、理論的には、ごくわずかです。パフォーマンスボーナス:最終的にレースを早期に終了することにより、非常にまれなケースで、ダーティキャッシュライン上の後続の呼び出し元が不必要に(ただし無害に)レースを行うのを防ぐことができます。
ダブルサンク
最後の超高速メソッドへの呼び出しは、前に示した静的拡張メソッドを介してサンクされます。これは、コンパイラがメタデータにバインドして伝播するために、コンパイル時に実際に存在するエントリポイントも何らかの方法で表す必要があるためです。ダブルサンクは、実行時まで実際に解決できないカスタマイズされたコードのIDEで、強く型付けされたメタデータとインテリセンスの圧倒的な利便性を支払うための小さな代償です。それでも、少なくとも静的にコンパイルされたコードと同じくらい高速に実行され、すべての呼び出しで大量のリフレクションを実行するよりもはるかに高速であるため、両方の長所を活用できます。