1. ホーム
  2. c#

StackExchange.Redisへのアクセス時にデッドロックが発生する。

2023-09-28 18:17:36

質問

を呼び出すと、デッドロックが発生します。 StackExchange.Redis .

何が起こっているのか正確には分からないので、非常にいらいらしています。この問題の解決や回避に役立つようなご意見をいただければ幸いです。


もし、あなたもこの問題を抱えていて、これをすべて読みたくないという場合に備えて。 を設定することをお勧めします。 PreserveAsyncOrderfalse .

ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;

そうすることで、おそらくこのQ&Aであるようなデッドロックが解決され、パフォーマンスも改善される可能性があります。


私たちの設定

  • コードは、コンソール アプリケーションまたは Azure Worker Role として実行されます。
  • REST api を使用し HttpMessageHandler を使っているので、エントリポイントは非同期です。
  • コードの一部はスレッドアフィニティ(単一スレッドが所有し、単一スレッドで実行する必要がある)を持っています。
  • コードの一部は非同期のみです。
  • を行っているのは sync-over-async そして 非同期-オーバーシンク アンチパターン (ミキシング awaitWait() / Result ).
  • Redisにアクセスするときだけ非同期メソッドを使っています。
  • .NET 4.5用のStackExchange.Redis 1.0.450を使用しています。

デッドロック

アプリケーションやサービスを起動すると、しばらくの間は正常に動作しますが、突然(ほとんど)すべての受信リクエストが機能しなくなり、応答が返されることはありません。これらのリクエストはすべて、Redisへの呼び出しが完了するのを待つためにデッドロックになっています。

興味深いことに、いったんデッドロックが発生すると、Redisへの呼び出しはすべてハングしますが、それらの呼び出しがスレッドプールで実行されている着信APIリクエストから行われた場合のみです。

また、優先度の低いバックグラウンド スレッドから Redis への呼び出しを行い、これらの呼び出しはデッドロックが発生した後でも機能し続けます。

スレッドプールスレッドで Redis に呼び出したときのみ、デッドロックが発生するように思われます。 私はもはや、これらの呼び出しがスレッドプールスレッド上で行われるという事実が原因だとは思いません。むしろ、非同期のRedis呼び出しはすべて を継続せずに、あるいは 同期セーフ 継続あり。 は、デッドロックの状況が発生した後でも動作し続けるでしょう。(参照 私が思うに起こること を参照してください)。

関連

  • StackExchange.Redis デッドロッキング

    混在によるデッドロック awaitTask.Result (といった具合です(私たちがやっているようなsync-over-async)。しかし、私たちのコードは同期コンテキストなしで実行されるので、ここでは適用されませんよね?

  • 同期と非同期のコードを安全に混ぜるには?

    そうです、そんなことをしてはいけないのです。しかし、私たちはそうしていますし、しばらくの間はそうし続けなければならないでしょう。非同期の世界に移行する必要のあるコードがたくさんあるのです。

    繰り返しますが、私たちは同期コンテキストを持っていないので、デッドロックの原因にはならないはずですよね?

    設定方法 ConfigureAwait(false) の前に await は影響を与えません。

  • StackExchange.Redisで非同期コマンドとTask.WhenAny待ちの後にタイムアウト例外が発生する

    これはスレッドハイジャック問題ですね。これについては現状どうなっているのでしょうか?ここが問題なのでしょうか?

  • StackExchange.Redisの非同期呼び出しがハングアップしています。

    Marcさんの回答より。

    ...Waitとawaitを混ぜるのは良いアイデアではありません。デッドロックに加えて、これはアンチパターンである "sync over async" "です。

    しかし、彼はこうも言っています。

    <ブロッククオート

    SE.Redisは内部でsync-contextをバイパスしているので(ライブラリコードとしては普通)、デッドロックは発生しないはずです。

    つまり、私の理解では、StackExchange.Redis は、私たちが sync-over-async アンチパターンを使用しているかどうかは関係ありません。でのデッドロックの原因になりうるので、推奨されないだけです。 その他の のコードでデッドロックを起こす可能性があるからです。

    しかし、この場合、私が知る限り、デッドロックは本当に StackExchange.Redis の内部で発生しているようです。間違っていたらご指摘ください。

デバッグの発見

デッドロックの原因は、どうやら ProcessAsyncCompletionQueue にあります。 の124行目 CompletionManager.cs .

そのコードのスニペットです。

while (Interlocked.CompareExchange(ref activeAsyncWorkerThread, currentThread, 0) != 0)
{
    // if we don't win the lock, check whether there is still work; if there is we
    // need to retry to prevent a nasty race condition
    lock(asyncCompletionQueue)
    {
        if (asyncCompletionQueue.Count == 0) return; // another thread drained it; can exit
    }
    Thread.Sleep(1);
}

デッドロック中に発見した activeAsyncWorkerThread は、Redis の呼び出しが完了するのを待っているスレッドの一つです。( は私たちのスレッド = スレッドプールで動作しているスレッド 私たちのコード ). つまり、上のループは永遠に続くと判断されます。

StackExchange.Redisは、このようなスレッドを待っています。 アクティブな非同期ワーカー スレッド と思っているスレッドを待っているのですが、実際にはそれとはまったく逆のスレッドなのです。

のせいなのでしょうかね。 スレッド ハイジャック問題 (これは完全に理解しているわけではありませんが)?

どうすればいいのでしょうか?

私が考えている主な2つの質問。

  1. ミキシング awaitWait() / Result は、同期コンテキストなしで実行されている場合でもデッドロックの原因となるのでしょうか?

  2. StackExchange.Redisのバグ/制限に遭遇しているのでしょうか?

修正の可能性は?

私のデバッグの発見から、それは問題があるように思われます。

next.TryComplete(true);

...上 の162行目 CompletionManager.cs は、ある状況下では、現在のスレッド (これは アクティブな非同期ワーカスレッド である) 現在のスレッドがさまよい、他のコードの処理を開始し、デッドロックが発生する可能性があります。

詳細を知らずに、この事実について考えるだけで、一時的に アクティブな非同期ワーカー スレッド の間に TryComplete を呼び出します。

こんな感じでいいんじゃないでしょうか。

// release the "active thread lock" while invoking the completion action
Interlocked.CompareExchange(ref activeAsyncWorkerThread, 0, currentThread);

try
{
    next.TryComplete(true);
    Interlocked.Increment(ref completedAsync);
}
finally
{
    // try to re-take the "active thread lock" again
    if (Interlocked.CompareExchange(ref activeAsyncWorkerThread, currentThread, 0) != 0)
    {
        break; // someone else took over
    }
}

私の一番の希望は マーク・グラベル がこれを読んでフィードバックしてくれることです :-)

同期コンテキストなし = デフォルトの同期コンテキスト

上で書いたように、このコードでは 同期コンテキスト . これは部分的にしか正しくありません。コードはコンソールアプリケーションまたは Azure Worker Role のいずれかとして実行されます。これらの環境では SynchronizationContext.Current null を実行していると書いたのは、そのためです。 を使わずに を実行していると書いた理由です。

しかし SynchronizationContext がすべてです。 というのは、実はそうではないことを知りました。

慣習により、スレッドの現在のSynchronizationContextがNULLの場合、暗黙のうちにデフォルトのSynchronizationContextを持つことになります。

UI ベース (WinForms や WPF) の同期コンテキストがデッドロックの原因となる可能性があるため、デフォルトの同期コンテキストはデッドロックの原因とはならないはずですが、これはスレッドの親和性を意味しないためです。

私が思うに、何が起こるのでしょうか。

メッセージが完了すると、その完了ソースは、それが 同期セーフ . もしそうであれば、完了アクションはインラインで実行され、すべてがうまくいきます。

そうでない場合、新たに割り当てられたスレッドプールのスレッドで完了アクションを実行することです。これもまた、次のような場合にうまく動作します。 ConnectionMultiplexer.PreserveAsyncOrderfalse .

しかし ConnectionMultiplexer.PreserveAsyncOrdertrue (デフォルト値) である場合、スレッドプールのスレッドは、その作業を 完了キュー を使用し、そのうちの少なくとも 1 つが アクティブな非同期ワーカスレッド であることを保証します。

あるスレッドが アクティブな非同期ワーカスレッド を使い果たすまで、その状態を維持します。 完了キュー .

問題は、完了アクションが シンクセーフではない (上記より) であり、なおかつ ブロックされてはならない を防ぐことになるので、他の 同期しない安全な メッセージが完了するのを妨げるからです。

他のメッセージは、完了アクションで完了されることに注意してください。 はシンクセーフ が設定されていても、他のメッセージは正常に動作し続けます。 アクティブな非同期ワーカスレッド がブロックされても、正常に動作し続けます。

私が提案した修正方法 (上記) は、この方法ではデッドロックを引き起こしません。 非同期完了の順序を維持する .

ということで、ここで出すべき結論は、たぶん を混ぜるのは安全ではない awaitResult / Wait() いつ PreserveAsyncOrdertrue 同期コンテキストなしで実行されているかどうかに関係なく、?

( 少なくとも、.NET 4.6を使うまでは、新しい TaskCreationOptions.RunContinuationsAsynchronously を使うまでは、おそらく )

どのように解決するのですか?

このデッドロックの問題に対して、私が発見した回避策です。

回避策その 1

デフォルトでは、StackExchange.Redisは結果メッセージを受信したのと同じ順序でコマンドが完了するようにします。このため、この質問で説明されているようなデッドロックが発生する可能性があります。

を設定することでその動作を無効にします。 PreserveAsyncOrderfalse .

ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;

これによりデッドロックが回避され、また 性能を向上させることができます。 .

デッドロックの問題に遭遇した人は、この回避策を試してみることをお勧めします。

非同期継続が、基礎となるRedis操作が完了したのと同じ順序で呼び出されるという保証はなくなります。しかし、なぜそれが信頼できるものなのか、私にはよくわかりません。


回避策その 2

デッドロックが発生するのは アクティブな非同期ワーカスレッド がコマンドを完了し、その完了タスクがインラインで実行されたときに発生します。

タスクがインラインで実行されるのを防ぐには、カスタムの TaskScheduler を使用して、確実に TryExecuteTaskInline が返す false .

public class MyScheduler : TaskScheduler
{
    public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        return false; // Never allow inlining.
    }

    // TODO: Rest of TaskScheduler implementation goes here...
}

優れたタスクスケジューラを実装することは、複雑な作業かもしれません。しかし、既存の実装が ParallelExtensionExtras ライブラリ ( NuGet パッケージ ) を使用したり、そこからインスピレーションを得ることができます。

タスクスケジューラが(スレッドプールからではなく)自身のスレッドを使用する場合、現在のスレッドがスレッドプールからでない限りインライン化を許可するのは良いアイデアかもしれません。これは アクティブな非同期ワーカスレッド は常にスレッドプールのスレッドであるためです。

public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
    // Don't allow inlining on a thread pool thread.
    return !Thread.CurrentThread.IsThreadPoolThread && this.TryExecuteTask(task);
}

もう一つのアイデアは、スケジューラをそのすべてのスレッドにアタッチすることです。 スレッドローカルストレージ .

private static ThreadLocal<TaskScheduler> __attachedScheduler 
                   = new ThreadLocal<TaskScheduler>();

このフィールドがスレッドの実行開始時に割り当てられ、終了時にクリアされることを確認する。

private void ThreadProc()
{
    // Attach scheduler to thread
    __attachedScheduler.Value = this;

    try
    {
        // TODO: Actual thread proc goes here...
    }
    finally
    {
        // Detach scheduler from thread
        __attachedScheduler.Value = null;
    }
}

そして、カスタムスケジューラによって "owned" されているスレッド上で行われる限り、タスクのインライン化を許可することができます。

public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
    // Allow inlining on our own threads.
    return __attachedScheduler.Value == this && this.TryExecuteTask(task);
}