18

COM オブジェクトが STA スレッドでインスタンス化される場合、スレッドは通常、他のスレッドとの間で呼び出しをマーシャリングするためにメッセージ ポンプを実装する必要があります (こちらを参照)。

メッセージを手動でポンピングするか、すべてではないが一部のスレッドブロッキング操作が待機中に COM 関連のメッセージを自動的にポンピングするという事実に依存することができます。多くの場合、ドキュメントはどれがどれであるかを判断するのに役立ちません (この関連する質問を参照してください)。

スレッド ブロック操作が STA で COM メッセージを送信するかどうかを判断するにはどうすればよいですか?

これまでの部分的なリスト:

ポンプを行うブロッキング操作*:

ポンピングしないブロック操作:

* Noseratio の回答によると、ポンプを実行する操作でさえ、非常に限定された未公開の COM 固有のメッセージ セットに対してポンプを実行するということです。

4

3 に答える 3

6

BlockingCollectionブロッキング中に実際にポンプします。次の質問に答えているときに、STA ポンピングに関する興味深い詳細がいくつかあることを知りました。

StaTaskScheduler および STA スレッド メッセージのポンピング

ただし、リストした他の API と同じように、非常に限定された未公開の COM 固有のメッセージ セットを送り出します。汎用の Win32 メッセージは送信されません (特殊なケースはWM_TIMERであり、これもディスパッチされません)。これは、フル機能のメッセージ ループを予期する一部の STA COM オブジェクトでは問題になる可能性があります。

これを試してみたい場合は、独自のバージョンの を作成しSynchronizationContext、 overrideSynchronizationContext.Waitを呼び出しSetWaitNotificationRequiredて、カスタムの同期コンテキスト オブジェクトを STA スレッドにインストールします。次に、内部にブレークポイントを設定し、Waitどの API が呼び出されるかを確認します。

の標準的なポンピング動作WaitOneは実際にどの程度制限されていますか? 以下は、UI スレッドでデッドロックを引き起こす典型的な例です。ここでは WinForms を使用していますが、同じ懸念が WPF にも当てはまります。

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();

        this.Load += (s, e) =>
        {
            Func<Task> doAsync = async () =>
            {
                await Task.Delay(2000);
            };

            var task = doAsync();
            var handle = ((IAsyncResult)task).AsyncWaitHandle;

            var startTick = Environment.TickCount;
            handle.WaitOne(4000);
            MessageBox.Show("Lapse: " + (Environment.TickCount - startTick));
        };
    }
}

タスクの完了には 2000 ミリ秒しかかかりませんが、メッセージ ボックスには 4000 ミリ秒までの経過時間が表示されます。

これは、継続コールバックが を介してスケジュールされているために発生awaitします。このメッセージはポンピングされず、タイムアウトします。WindowsFormsSynchronizationContext.PostControl.BeginInvokePostMessageRegisterWindowMessagehandle.WaitOne

を使用handle.WaitOne(Timeout.Infinite)すると、古典的なデッドロックが発生します。

それでは、明示的なポンピングを使用して のバージョンを実装しましょうWaitOne(そしてそれを と呼びますWaitOneAndPump):

public static bool WaitOneAndPump(
    this WaitHandle handle, int millisecondsTimeout)
{
    var startTick = Environment.TickCount;
    var handles = new[] { handle.SafeWaitHandle.DangerousGetHandle() };

    while (true)
    {
        // wait for the handle or a message
        var timeout = (uint)(Timeout.Infinite == millisecondsTimeout ?
                Timeout.Infinite :
                Math.Max(0, millisecondsTimeout + 
                    startTick - Environment.TickCount));

        var result = MsgWaitForMultipleObjectsEx(
            1, handles,
            timeout,
            QS_ALLINPUT,
            MWMO_INPUTAVAILABLE);

        if (result == WAIT_OBJECT_0)
            return true; // handle signalled
        else if (result == WAIT_TIMEOUT)
            return false; // timed-out
        else if (result == WAIT_ABANDONED_0)
            throw new AbandonedMutexException(-1, handle);
        else if (result != WAIT_OBJECT_0 + 1)
            throw new InvalidOperationException();
        else
        {
            // a message is pending 
            if (timeout == 0)
                return false; // timed-out
            else
            {
                // do the pumping
                Application.DoEvents();
                // no more messages, raise Idle event
                Application.RaiseIdle(EventArgs.Empty);
            }
        }
    }
}

元のコードを次のように変更します。

var startTick = Environment.TickCount;
handle.WaitOneAndPump(4000);
MessageBox.Show("Lapse: " + (Environment.TickCount - startTick));

await継続メッセージが によってポンピングされApplication.DoEvents()、タスクが完了し、そのハンドルが通知されるため、経過時間は約 2000 ミリ秒になります。

とは言っても、本番コードのようなものを使用することはお勧めしませんWaitOneAndPump(ごく少数の特定のケースを除いて)。これは、UI の再入可能性など、さまざまな問題の原因となります。これらの問題が、Microsoft が標準のポンピング動作を、COM マーシャリングに不可欠な特定の COM 固有のメッセージのみに限定した理由です。

于 2014-02-05T09:46:24.783 に答える