問題の声明
したがって、解決する問題は次のように定式化できます:スレッドが開始されたコンテキストに関係なく、特定の(ターゲット)ストリームのコンテキストでコード(実行要素)を実行できるメカニズムを開発する必要があります。 ターゲットスレッドは一度に1つの実行要素しか実行できないため、派生要件は実行要素のキューイングです。
実装オプション
頭に浮かぶ最初のオプションは、Actionクラスのオブジェクトのキューの使用です。
ConcurrentQueue<Action> _actions;
同時に、クライアントスレッドはそれに実行要素を追加します。
_actions.Enqueue(() => { < > });
そして、ターゲットスレッドはそれらを受信順に実行します。
Action action; if (_actions.TryDequeue()) { action(); }
もちろん、この実装オプションには存在する権利がありますが、この場合の正面実装はリソースの使用率と生産性の点で最適とはほど遠いでしょうし、より高度な技術は開発とテストのためにはるかに多くの時間を必要とし、おそらく自転車の発明になるでしょう。 したがって、この記事では、.NET Frameworkが提供するツールを使用して、問題を解決するよりエレガントな方法を検討します。 つまり、System.Windows.Threading.Dispatcherは、おそらく上記の説明と同様に機能し、排他ロックを使用します。
実装
外部ライブラリへのインターフェイスを実装するExternalInterfaceクラスがあり、そのメソッドを別のスレッドで呼び出す必要があるとします。
internal static class ExternalInterface { public static void Method1() { throw new NotImplementedException(); } public static int Method2() { throw new NotImplementedException(); } public static int Method3() { throw new NotImplementedException(); } private static void ExternalMethod1() { // Thread.Sleep(1000); } private static int ExternalMethod2() { // Thread.Sleep(1000); return 1; } private static void ExternalMethod3() { // Thread.Sleep(1000); } }
ExternalMethod1、ExternalMethod2、ExternalMethod3は外部ライブラリのインポートされたメソッドであり、Method1、Method2、Method3は同じ割り当てられたストリームのコンテキストでインポートされたメソッドを実行することを唯一のタスクとするクラスのインターフェイスメソッドです。
また、ExternalInterfaceメソッドがさまざまなスレッドから呼び出されるクライアントクラス(最も単純な場合、コンソールアプリケーションのProgramクラス)もあります。
class Program { static void ThreadMethod1() { ExternalInterface.Method1(); } static void ThreadMethod2() { ExternalInterface.Method2(); } static void ThreadMethod3() { ExternalInterface.Method3(); } static void Main(string[] args){ } }
次に、単一のスレッドでインポートされたメソッドの呼び出しを整理することに移りましょう。 まず、インポートされたメソッドが呼び出されるコンテキストでストリームを作成し、呼び出しを待機する準備をします。
// private static Thread _dispatchingThread; // Dispatcher private static Dispatcher _dispatchObject; public static void Open() { // _dispatchingThread = new Thread(DispatchingThreadMethod); _dispatchingThread.Start(); } private static void DispatchingThreadMethod() { Console.WriteLine(Thread.CurrentThread.ManagedThreadId); // , // ... // Dispatcher' _dispatchObject = Dispatcher.CurrentDispatcher; // Dispatcher.Run(); // , // ... }
次に、実行要素のターゲットストリームへの転送を単純化します。
private void Dispatch(Action action) { if (_dispatchObject != null) { _dispatchObject.Invoke(action); } }
ただし、この実装では、複数のスレッドが実行スレッドをターゲットスレッドに渡すことができるため、呼び出しスレッドのデッドロックが発生します(この動作は特定のネイティブライブラリを使用する場合にのみ観察され、この例では再現されません。つまり、このステップはスキップされる場合があります)。 排他ロックを使用して要求をキューに入れることができます。 コードを次のように変更します。
private void Dispatch(Action action) { if (_dispatchObject != null) { lock (_dispatchObject) { _dispatchObject.Invoke(action); } } }
次に、インターフェイスメソッドを実装します。
public static void Method1() { Dispatch(() => ExternalMethod1()); } public static int Method2() { int result = 0; Dispatch(() => { result = ExternalMethod2(); }); return result; } public static void Method3() { Dispatch(() => ExternalMethod3()); }
the_dispatchingThreadスレッドを正しく完了するには、ExternalInterfaceクラスに別のメソッドを追加します。
public static void Close() { _dispatchObject.InvokeShutdown(); }
すべてのメソッドが同じスレッドで呼び出されることを証明するには、次のコードを実行します。
static void Main(string[] args) { ExternalInterface.Open(); // Dispatcher // Thread.Sleep(1000); new Thread(ThreadMethod1).Start(); new Thread(ThreadMethod2).Start(); new Thread(ThreadMethod3).Start(); Console.ReadKey(); ExternalInterface.Close(); }
結果は、同じスレッド識別子の4倍の印刷になります。つまり、すべてのメソッド呼び出しは1つのスレッドのコンテキストで行われました。
結論として、この記事の目的は問題を解決するためのメカニズムのみを示すことであったため、深刻なプロジェクトでこのコードを使用するにはいくつかの改善が必要になると言いたいと思います。 また、上記の例はこのメカニズムの範囲を制限しません。 このアプローチは、1つのストリームからの作業が必要なあらゆる状況に適用できますが、メインプログラムストリームからの作業は不可能です。 同様のアプローチは、特定のスレッドの実行要素からのキューイングにも適しています。