Как избежать блокировки читателей 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 ответа
Я не могу помочь, но я считаю, что это ошибка.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
заблокирован навсегда (или, в случае этого примера, на эти 50 секунд доT1
оставляет блокировку чтения).
AFAICT, ExitMyLock
в ReaderWriterLockSlim
"sWaitOnEvent
должно было ExitAndWakeUpAppropriateWaiters
,
Я согласен с Mormegil
ответ, что ваше наблюдаемое поведение не соответствует тому, что говорится в документации для метода ReaderWriterLockSlim.TryEnterWriteLock (Int32):
В то время как потоки заблокированы в ожидании входа в режим записи, дополнительные потоки, которые пытаются войти в режим чтения или блок режима обновления, пока все потоки, ожидающие перехода в режим записи, не истекли по времени или не вошли в режим записи, а затем вышли из него.
Обратите внимание, что есть 2 документированных способа, которыми другие потоки, ожидающие входа в режим чтения, перестанут ждать:
TryEnterWriteLock
время вышло.TryEnterWriteLock
успешно, а затем снимает блокировку, вызываяExitWriteLock
,
Второй сценарий работает, как и ожидалось. Но первое (сценарий тайм-аута) не так, как ясно показывает пример кода.
Почему это не работает?
Почему потоки, пытающиеся войти в режим чтения, продолжают ждать, даже после истечения времени ожидания попытки входа в режим записи?
Потому что для того, чтобы разрешить ожидающим потокам входить в режим чтения, должны произойти 2 вещи. Поток, ожидающий блокировки записи, должен:
- Сбросьте флаг, указывающий, что он больше не ожидает входа в режим записи. Это делается путем вызова ClearWritersWaiting(), если вы посмотрите на исходный код.
- Ожидающие потоки сигналов о том, что они должны повторить попытку получения требуемой блокировки. Эта сигнализация выполняется путем вызова ExitAndWakeUpAppresponWaiters ().
Выше, что должно произойти. Но на практике, когда TryEnterWriteLock
по истечении времени выполняется только первый шаг (сброс флага), но не 2-й (сигнализация ожидающих потоков, чтобы повторить попытку получения блокировки). В результате в вашем случае поток, пытающийся получить блокировку чтения, продолжает ждать бесконечно, потому что ему никогда не "говорят", что он должен проснуться и снова проверить флаг.
Как указано Mormegil
, меняя звонок с ExitMyLock()
в ExitAndWakeUpAppropriateWaiters()
в этой строке кода, кажется, все, что нужно для решения проблемы. Поскольку исправление кажется таким простым, я также склонен думать, что это просто ошибка, которую упустили из виду.
Как эта информация полезна?
Понимание причины позволяет нам понять, что влияние "ошибочного" поведения несколько ограничено. Это только заставляет потоки блокировать "на неопределенный срок", если они пытаются получить блокировку после того, как какой-то поток вызывает 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 пытается войти в режим WRITE...
02010: T3 пытается войти в режим чтения...
03010: истекло время T2! Невозможно войти в режим ЗАПИСИ.
04013: T4 пытается войти в режим чтения...
04013: T4 успешно вошел в режим чтения! Не был затронут "ошибка" тайм-аута T2
04013: T4 вышел из режима чтения. (Побочный эффект: просыпаются любые другие темы официанта)
04013: T3 в конце концов перешел в режим чтения! ExitReadLock() от T4 разблокировал меня.
04013: T3 вышел из режима чтения.
10005: T1 вышел из режима чтения.
Также обратите внимание, что T4
нить читателя строго не требуется для разблокировки T3
, T1
в конечном итоге ExitReadLock
также разблокировал T3
,
Последние мысли
Несмотря на то, что текущее поведение не является идеальным и выглядит как ошибка в библиотеке.NET, в реальном сценарии поток (ы) читателя (ей), который в первую очередь вызвал тайм-аут потока писателя, в конечном итоге завершится Режим чтения. А это, в свою очередь, разблокирует все ожидающие читательские потоки, которые могут зависнуть из-за "ошибки". Таким образом, реальное воздействие "жука" должно быть минимальным.
Тем не менее, как предлагается в комментариях, чтобы сделать ваш код еще более надежным, было бы неплохо изменить ваш EnterReadLock()
обращение к TryEnterReadLock
с разумным значением времени ожидания и вызывается внутри цикла. Таким образом, вы можете иметь некоторую меру контроля над максимальным периодом времени, в течение которого поток чтения будет "зависать".