.net 当写入程序试图进入写锁时,如何避免阻塞ReaderWriterLockSlim读卡器

.net 当写入程序试图进入写锁时,如何避免阻塞ReaderWriterLockSlim读卡器,.net,multithreading,readerwriterlockslim,.net,Multithreading,Readerwriterlockslim,我正在使用ReaderWriterLockSlim来保护一些操作。我更喜欢读写器,这样当读写器长时间持有锁,而写写器试图获取写锁时,以后的读写器不会被写器的尝试所阻止(如果写器在lock.EnterWriteLock()上被阻止,则会发生这种情况) 为此,我认为编写器可以使用TryEnterWriteLock,在循环中有一个短的超时,这样后续的读卡器仍然能够获得读锁,而编写器不能。然而,令我惊讶的是,我发现调用TryEnterWriteLock失败会改变锁的状态,从而阻止未来的读卡器。概念验证代

我正在使用
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”)没有获得Writer锁,也没有在
EnterWriteLock
调用中,线程3也会被永久阻止。我可以在
ReaderWriterLock
中看到类似的行为


我做错什么了吗?如果没有,当一个作者排队时,我有什么选择来帮助读者呢?

我无能为力,但我相信这是一个.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();
以下事件发生的顺序很简单,没有锁定车队,没有比赛,没有环路或其他任何事情

  • T1
    获取读锁
  • T2
    尝试获取写锁并阻塞,等待超时(因为
    T1
    持有锁)
  • T3
    尝试获取读锁并阻塞(因为
    T2
    在等待写锁时被阻塞,根据文档,这意味着所有其他读卡器都被阻塞,直到超时)
  • T2
    的超时过期。根据文档,
    T3
    现在应该唤醒并获取读取锁。但是,这种情况不会发生,
    T3
    被永久阻止(或者,在本例中,在
    T1
    离开读取锁定之前的50秒内)

  • 在AICT中,
    exitmolock
    应该是
    exit并唤醒适当的服务员

    我同意
    Mormegil
    的回答,即您观察到的行为似乎不符合以下州的文件:

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

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

    • TryEnterWriteLock
      超时
    • TryEnterWriteLock
      成功,然后通过调用
      ExitWriteLock
      释放锁
    第二个场景按预期工作。但是第一个(超时场景)没有,正如您的代码示例清楚地显示的那样

    为什么不起作用?

    为什么尝试进入读取模式的线程会一直等待,即使在尝试进入写入模式超时之后也是如此

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

    • 重置指示不再等待进入写入模式的标志。这是通过打电话来完成的
    • 向等待的线程发出信号,指示它们应该重试获取所需的锁。此信令由对的调用执行
    以上是应该发生的事情。但实际上,当
    TryEnterWriteLock
    超时时,只执行第一步(重置标志),而不执行第二步(向等待的线程发出重试获取锁的信号)。因此,在您的例子中,试图获取读锁的线程一直在无限期地等待,因为它从未被“告知”应该唤醒并再次检查标志

    正如
    Mormegil
    所指出的,将中的调用从
    exitmolock()
    更改为
    exitandwakeuppropertedwaiters()
    似乎就是解决问题所需的全部。因为修复看起来很简单,我也倾向于认为这只是一个被忽略的bug

    这些信息如何有用?

    理解原因使我们认识到“buggy”行为的影响在一定程度上是有限的。如果线程在某个线程调用
    TryEnterWriteLock
    之后,而在
    TryEnterWriteLock
    调用超时之前,试图获取锁,则它只会导致线程“无限期”阻塞。而且它不会无限期地阻塞。当其他线程正常释放其锁时,它最终会解除阻塞,这是本例中的预期情况

    这也意味着,在
    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正在尝试进入读取模式…
    00001:T1成功进入读取模式
    01011:T2正在尝试进入写入模式…
    02010:T3试图进入读取模式…
    03010:T2超时!无法进入写入模式。
    04013:T4试图进入读取模式…
    04013:T4成功进入读取模式!未受T2超时“错误”的影响
    04013:T4退出读取模式。(副作用:唤醒任何其他服务员线程)
    04013:T3毕竟进入了读取模式!T4的ExitReadLock()解除了我的阻止。
    04013:T3退出读取模式。
    10005:T1已退出读取模式

    还请注意,
    T4
    读卡器线程并不是解除阻塞所必需的