10

ReaderWriterLockSlim一部の操作を保護するために使用しています。ライターよりもリーダーを優先したいと思います。これにより、リーダーがロックを長時間保持し、ライターが書き込みロックを取得しようとしているときに、その先のリーダーがライターの試みによってブロックされないようにすることができます (代わりに、ライターは でブロックされましたlock.EnterWriteLock())。

この目的のために、ライターがTryEnterWriteLockループ内で短いタイムアウトを使用できるので、後続のリーダーは引き続き読み取りロックを取得でき、ライターは取得できないと考えました。しかし、驚いたことに、 の呼び出しに失敗するとTryEnterWriteLock、ロックの状態が変更され、今後のリーダーがブロックされることがわかりました。概念実証コード:

System.Threading.ReaderWriterLockSlim myLock = new System.Threading.ReaderWriterLockSlim(System.Threading.LockRecursionPolicy.NoRecursion);

System.Threading.Thread t1 = new System.Threading.Thread(() =>
{
    Console.WriteLine("T1:{0}: entering read lock...", DateTime.Now);
    myLock.EnterReadLock();
    Console.WriteLine("T1:{0}: ...entered read lock.", DateTime.Now);

    System.Threading.Thread.Sleep(10000);
});

System.Threading.Thread t2 = new System.Threading.Thread(() =>
{
    System.Threading.Thread.Sleep(1000);

    while (true)
    {
        Console.WriteLine("T2:{0}: try-entering write lock...", DateTime.Now);
        bool result = myLock.TryEnterWriteLock(TimeSpan.FromMilliseconds(1500));
        Console.WriteLine("T2:{0}: ...try-entered write lock, result={1}.", DateTime.Now, result);

        if (result)
        {
            // Got it!
            break;
        }

        System.Threading.Thread.Yield();
    }

    System.Threading.Thread.Sleep(9000);
});

System.Threading.Thread t3 = new System.Threading.Thread(() =>
{
    System.Threading.Thread.Sleep(2000);

    Console.WriteLine("T3:{0}: entering read lock...", DateTime.Now);
    myLock.EnterReadLock();
    Console.WriteLine("T3:{0}: ...entered read lock!!!!!!!!!!!!!!!!!!!", DateTime.Now);

    System.Threading.Thread.Sleep(8000);
});

このコードの出力は次のとおりです。

T1:18-09-2015 16:29:49: entering read lock...
T1:18-09-2015 16:29:49: ...entered read lock.
T2:18-09-2015 16:29:50: try-entering write lock...
T3:18-09-2015 16:29:51: entering read lock...
T2:18-09-2015 16:29:51: ...try-entered write lock, result=False.
T2:18-09-2015 16:29:51: try-entering write lock...
T2:18-09-2015 16:29:53: ...try-entered write lock, result=False.
T2:18-09-2015 16:29:53: try-entering write lock...
T2:18-09-2015 16:29:54: ...try-entered write lock, result=False.
T2:18-09-2015 16:29:54: try-entering write lock...
T2:18-09-2015 16:29:56: ...try-entered write lock, result=False.
T2:18-09-2015 16:29:56: try-entering write lock...
T2:18-09-2015 16:29:57: ...try-entered write lock, result=False.
T2:18-09-2015 16:29:57: try-entering write lock...
T2:18-09-2015 16:29:59: ...try-entered write lock, result=False.
T2:18-09-2015 16:29:59: try-entering write lock...

ご覧のとおり、スレッド 2 (「ライター」) はライター ロックを取得しておらず、EnterWriteLock呼び出し中ではありませんが、スレッド 3 は完全にブロックされます。で同様の動作が見られReaderWriterLockます。

私は何か間違ったことをしていますか?そうでない場合、ライターがキューに入れられたときにリーダーを優先するために必要なオプションは何ですか?

4

2 に答える 2

2

観察された動作がReaderWriterLockSlim.TryEnterWriteLock メソッド (Int32)Mormegilのドキュメントに記載されている内容に準拠していないようであるという の回答に同意します。

スレッドが書き込みモードに入るのを待ってブロックされている間、読み取りモードまたはアップグレード可能モードに入ろうとする追加のスレッドは、書き込みモードに入るのを待っているすべてのスレッドがタイムアウトするか、書き込みモードに入ってから終了するまでブロックされます。

読み取りモードに入るのを待っている他のスレッドが待機を停止する方法が 2 つ文書化されていることに注意してください。

  • TryEnterWriteLockタイムアウトします。
  • TryEnterWriteLock成功し、 を呼び出してロックを解除しますExitWriteLock

2 番目のシナリオは期待どおりに機能します。しかし、コード サンプルが明確に示すように、1 つ目 (タイムアウト シナリオ) はそうではありません。

うまくいかないのはなぜですか?

書き込みモードに入ろうとしてタイムアウトになった後でも、読み取りモードに入ろうとしているスレッドが待機し続けるのはなぜですか?

待機中のスレッドが読み取りモードに入るには、2 つのことが必要なためです。書き込みロックの待機がタイムアウトしたスレッドは、次のことを行う必要があります。

  • 書き込みモードに入るのを待機していないことを示すフラグをリセットします。ソースコードを見ると、これは ClearWritersWaiting() を呼び出すことによって行われます。
  • 必要なロックの取得を再試行する必要があることを待機中のスレッドに通知します。このシグナリングは、ExitAndWakeUpAppropriateWaiters()への呼び出しによって実行されます。

上記が起こるべきことです。しかし実際には、タイムアウトした場合TryEnterWriteLock、最初のステップ (フラグのリセット) のみが実行され、2 番目のステップ (待機中のスレッドにロックの取得を再試行するように通知する) は実行されません。その結果、あなたの場合、読み取りロックを取得しようとしているスレッドは、ウェイクアップしてフラグを再度チェックする必要があると「言われる」ことがないため、無期限に待機し続けます。

で指摘されているように、このコード行でMormegil呼び出しを から に変更ExitMyLock()するだけで、問題を解決できます。修正が非常に簡単に見えるため、これは見過ごされた単なるバグであると考える傾向もあります。ExitAndWakeUpAppropriateWaiters()

この情報はどのように役立ちますか?

原因を理解することで、「バグのある」動作の影響の範囲がいくぶん限定されていることがわかります。一部のスレッドがを呼び出した、呼び出しがタイムアウトする前に、スレッドがロックを取得しようとした場合にのみ、スレッドが「無期限に」ブロックされます。そして、実際には無期限にブロックするわけではありません。他のスレッドがロックを正常に解放するとすぐに、最終的にブロックを解除します。これは、この場合に予想されるシナリオです。TryEnterWriteLockTryEnterWriteLock

これは、タイムアウト後 に READ モードに入ろうとするスレッドTryEnterWriteLockが問題なく実行できることも意味します。

これを説明するために、次のコード スニペットを実行します。

private static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
private static Stopwatch stopwatch = new Stopwatch();

static void Log(string logString)
{
    Console.WriteLine($"{(long)stopwatch.Elapsed.TotalMilliseconds:D5}: {logString}");
}

static void Main(string[] args)
{
    stopwatch.Start();

    // T1 - Initial reader
    new Thread(() =>
    {
        Log("T1 trying to enter READ mode...");
        rwLock.EnterReadLock();
        Log("T1 entered READ mode successfully!");
        Thread.Sleep(10000);
        rwLock.ExitReadLock();
        Log("T1 exited READ mode.");
    }).Start();

    // T2 - Writer times out.
    new Thread(() =>
    {
        Thread.Sleep(1000);
        Log("T2 trying to enter WRITE mode...");
        if (rwLock.TryEnterWriteLock(2000))
        {
            Log("T2 entered WRITE mode successfully!");
            rwLock.ExitWriteLock();
            Log("T2 exited WRITE mode.");
        }
        else
        {
            Log("T2 timed out! Unable to enter WRITE mode.");
        }
    }).Start();

    // T3 - Reader blocks longer than it should, BUT...
    //      is eventually unblocked by T4's ExitReadLock().
    new Thread(() =>
    {
        Thread.Sleep(2000);
        Log("T3 trying to enter READ mode...");
        rwLock.EnterReadLock();
        Log("T3 entered READ mode after all!  T4's ExitReadLock() unblocked me.");
        rwLock.ExitReadLock();
        Log("T3 exited READ mode.");
    }).Start();

    // T4 - Because the read attempt happens AFTER T2's timeout, it doesn't block.
    //      Also, once it exits READ mode, it unblocks T3!
    new Thread(() =>
    {
        Thread.Sleep(4000);
        Log("T4 trying to enter READ mode...");
        rwLock.EnterReadLock();
        Log("T4 entered READ mode successfully! Was not affected by T2's timeout \"bug\"");
        rwLock.ExitReadLock();
        Log("T4 exited READ mode. (Side effect: wakes up any other waiter threads)");
    }).Start();
}

出力:

00000: T1 が READ モードに入ろうとしています...
00001: T1 が正常に READ モードに入りました!
01011: T2 が WRITE モードに入ろうとしています...
02010: T3 が READ モードに入ろうとしています...
03010: T2 がタイムアウトしました! 書き込みモードに入ることができません。
04013: T4 が READ モードに入ろうとしています...
04013: T4 は正常に READ モードに入りました! T2 のタイムアウト「バグ」の影響を受けませんでした
04013: T4 が READ モードを終了しました。(副作用: 他のすべての待機スレッドを起動します)
04013: 結局、T3 は READ モードに入りました! T4 の ExitReadLock() でブロックが解除されました。
04013: T3 が READ モードを終了しました。
10005: T1 が READ モードを終了しました。

また、T4ブロックを解除するためにリーダー スレッドが厳密に必要なわけではないことにも注意してくださいT3T1の最終的なExitReadLockブロックも解除されT3ます。

最終的な考え

現在の動作は理想的とは言えず、.NET ライブラリのバグのように感じますが、実際のシナリオでは、最初にライター スレッドをタイムアウトさせたリーダー スレッドが最終的に終了します。読み取りモード。これにより、「バグ」が原因でスタックしている可能性のある待機中のリーダー スレッドのブロックが解除されます。したがって、「バグ」の実際の影響は最小限に抑える必要があります。

それでも、コメントで示唆されているように、コードの信頼性をさらに高めるために、EnterReadLock()呼び出しをTryEnterReadLock妥当なタイムアウト値に変更し、ループ内で呼び出すことは悪い考えではありません。そうすれば、リーダースレッドが「スタック」したままになる最大時間をある程度制御できます。

于 2015-09-21T13:36:47.827 に答える