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

3

我忍不住,但我相信这是一个 .NET Framework 错误(更新:我已经报告了这个错误)。以下简单的程序(上述程序的简化版本)说明了这一点:

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();

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

  1. T1获取读锁。
  2. T2尝试获取写锁并阻塞,等待超时(因为T1持有锁)。
  3. T3尝试获取读锁并阻塞(因为T2等待写锁被阻塞,并且根据文档,这意味着所有进一步的读取器都被阻塞,直到超时)。
  4. T2的超时时间到期。根据文档,T3现在应该唤醒并获取读锁。然而,这不会发生并且T3被永远阻塞(或者,在这个例子中,在T1离开读锁之前的 50 秒内)。

AFAICT,ExitMyLockin应该ReaderWriterLockSlimWaitOnEventExitAndWakeUpAppropriateWaiters.

于 2015-09-18T16:03:55.990 回答
2

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

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

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

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

第二种情况按预期工作。但是第一个(超时场景)没有,正如您的代码示例清楚地显示的那样。

为什么它不起作用?

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

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

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

正如 指出的那样,在这行代码Mormegil中将调用 from 更改为ExitMyLock()似乎是解决问题所需的全部内容。因为修复看起来很简单,我也倾向于认为这只是一个被忽视的错误。ExitAndWakeUpAppropriateWaiters()

这些信息有何用处?

了解原因让我们意识到“错误”行为的影响范围有限。如果线程在某个线程调用之后TryEnterWriteLock调用超时之前尝试获取锁,它只会导致线程“无限期地”阻塞TryEnterWriteLock。而且它并没有真正无限期地阻塞。一旦其他线程正常释放它的锁,它最终会解除阻塞,这将是这种情况下的预期场景。

这也意味着任何在超时 TryEnterWriteLock尝试进入 READ 模式的线程都可以毫无问题地这样做。

为了说明这一点,运行以下代码片段:

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 超时“错误”
04013 的影响:T4 退出了读取模式。(副作用:唤醒任何其他服务员线程)
04013:毕竟 T3 进入了 READ 模式!T4 的 ExitReadLock() 解锁了我。
04013: T3 退出读取模式。
10005:T1 退出读取模式。

另请注意,T4为了解除阻塞,并不严格要求阅读器线程T3T1的最终ExitReadLock也将畅通无阻T3

最后的想法

尽管当前的行为不太理想,并且确实感觉像是 .NET 库中的错误,但在现实生活中,最初导致写入器线程超时的读取器线程最终将退出读模式。反过来,这将解除阻塞可能由于“错误”而被卡住的任何等待阅读器线程。因此,“错误”对现实生活的影响应该是最小的。

尽管如此,正如评论中所建议的那样,为了使您的代码更加可靠,将您的EnterReadLock()调用更改为TryEnterReadLock具有合理的超时值并在循环内调用并不是一个坏主意。这样,您可以对阅读器线程将保持“卡住”的最长时间进行某种程度的控制。

于 2015-09-21T13:36:47.827 回答