如何在writer尝试输入写锁定时避免阻塞ReaderWriterLockSlim读取器

时间:2015-09-18 14:38:02

标签: .net multithreading readerwriterlockslim

我正在使用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(“Writer”)没有获得编写器锁定而且它不在EnterWriteLock调用中,线程3也会被阻止。我可以看到与ReaderWriterLock类似的行为。

我做错了吗?如果没有,当作家排队时,我有什么选择支持读者呢?

2 个答案:

答案 0 :(得分:3)

我忍不住但我相信这是一个.NET Framework错误(更新:我有reported the bug)。以下简单程序(上述简化版)说明了:

setContentEncoding()

以下顺序发生,没有锁定车队,没有比赛,没有循环或任何事情。

  1. var myLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); var t1 = new Thread(() => { Console.WriteLine("T1:{0}: entering read lock...", DateTime.Now); myLock.EnterReadLock(); Console.WriteLine("T1:{0}: ...entered read lock.", DateTime.Now); Thread.Sleep(50000); Console.WriteLine("T1:{0}: exiting", DateTime.Now); myLock.ExitReadLock(); }); var t2 = new Thread(() => { Thread.Sleep(1000); Console.WriteLine("T2:{0}: try-entering write lock...", DateTime.Now); bool result = myLock.TryEnterWriteLock(3000); Console.WriteLine("T2:{0}: ...try-entered write lock, result={1}.", DateTime.Now, result); Thread.Sleep(50000); if (result) { myLock.ExitWriteLock(); } Console.WriteLine("T2:{0}: exiting", DateTime.Now); }); var t3 = new Thread(() => { Thread.Sleep(2000); Console.WriteLine("T3:{0}: entering read lock...", DateTime.Now); myLock.EnterReadLock(); Console.WriteLine("T3:{0}: ...entered read lock!!!!!!!!!!!!!!!!!!!", DateTime.Now); Thread.Sleep(50000); myLock.ExitReadLock(); Console.WriteLine("T3:{0}: exiting", DateTime.Now); }); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); 获取了一个读锁定。
  2. T1尝试获取写锁并阻塞,等待超时(因为T2持有锁)。
  3. T1尝试获取读锁和阻塞(因为T3被阻止等待写锁定,并且根据文档,这意味着所有其他读者都被阻止,直到超时)。
  4. T2的超时到期。根据文档,T2现在应该唤醒并获取读锁定。但是,这不会发生,T3将永久阻止(或者,在此示例的情况下,在T3离开读锁定之前的50秒内。
  5. AFAICT,ReaderWriterLockSlim’s WaitOnEvent中的T1应为ExitMyLock

答案 1 :(得分:2)

我同意Mormegil的答案,即您观察到的行为似乎不符合ReaderWriterLockSlim.TryEnterWriteLock Method (Int32)州的文档:

  

当线程被阻塞等待进入写入模式时,尝试进入读取模式或可升级模式的其他线程会阻塞,直到等待进入写入模式的所有线程都超时或进入写入模式然后退出它。

请注意,等待进入读取模式的其他线程有两种记录方式将停止等待:

  • TryEnterWriteLock超时。
  • TryEnterWriteLock成功,然后通过调用ExitWriteLock释放锁定。

第二种情况按预期工作。但是第一个(超时场景)并没有,因为你的代码示例清楚地显示了。

为什么不起作用?

为什么尝试进入读取模式的线程会一直等待,即使在尝试进入写入模式时超时?

因为,为了让等待的线程进入读取模式,需要做两件事。超时等待写锁定的线程需要:

以上是 应该 发生的事情。但实际上,当TryEnterWriteLock超时时,只执行第一步(重置标志),而不执行第二步(发信号等待线程重试获取锁)。因此,在您的情况下,尝试获取读锁定的线程会无限期地等待,因为它永远不会被告知"它应该醒来并再次检查旗帜。

正如Mormegil所指出的,将ExitMyLock()中的来电更改为this line of code中的ExitAndWakeUpAppropriateWaiters()似乎是解决问题所需的全部内容。因为修复看起来很简单,我也倾向于认为这只是一个被忽视的错误。

这些信息有用吗?

了解原因使我们能够意识到"马车"行为的影响范围有限。它只会导致线程无限期地阻塞"如果他们尝试获取锁定 某个线程调用TryEnterWriteLock,但 之前 TryEnterWriteLock 1}}调用超时。它并没有真正阻止无限期。一旦其他一些线程正常释放锁定,它最终会解锁,这在这种情况下就是预期的情况。

这也意味着任何试图在 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超时!无法进入WRITE模式   04013:T4尝试进入READ模式...
  04013:T4成功进入READ模式!没有受到T2超时的影响" bug"
  04013:T4退出READ模式。 (副作用:唤醒任何其他服务员线程)
  04013:T3毕竟进入了READ模式! T4的ExitReadLock()解锁了我   04013:T3退出READ模式   10005:T1退出READ模式。

另请注意,T4读者线程并非严格要求才能取消阻止T3T1的最终ExitReadLock也会取消阻止T3

最后的想法

虽然当前的行为不太理想,并且确实感觉像是.NET库中的错误,但在现实生活中,读者(s)线程导致编写器线程首先超时最终会退出READ模式。而这反过来会阻止任何等待读者线程,因为" bug"可能会被卡住。所以" bug"的真实生活影响应该是最小的。

然而,正如评论中所建议的那样,为了使您的代码更加可靠,使用合理的超时值将EnterReadLock()调用更改为TryEnterReadLock并不是一个坏主意。在循环内调用。这样,您就可以对读取器线程停留的最长时间进行一定程度的控制,以及#34;卡住"。