Потокобезопасное выполнение с использованием System.Threading.Timer и Monitor
Используя System.Threading.Timer
приводит к тому, что темы из ThreadPool
, это означает, что если интервал выполнения для таймера истекает, когда поток все еще обрабатывает в порядке предыдущего запроса, то тот же обратный вызов будет делегирован для выполнения в другом потоке. Это, очевидно, вызовет проблемы в большинстве случаев, если обратный вызов не распознается вновь входящим, но мне интересно, как это сделать наилучшим (то есть безопасным) способом.
Допустим, у нас есть следующее:
ReaderWriterLockSlim OneAtATimeLocker = new ReaderWriterLockSlim();
OneAtATimeCallback = new TimerCallback(OnOneAtATimeTimerElapsed);
OneAtATimeTimer = new Timer(OneAtATimeCallback , null, 0, 1000);
Если весь Шебанг будет заблокирован, как таковой:
private void OnOneAtATimeTimerElapsed(object state)
{
if (OneAtATimeLocker.TryEnterWriteLock(0))
{
//get real busy for two seconds or more
OneAtATimeLocker.ExitWriteLock();
}
}
Или следует управлять только входом и удалять "нарушителей" как таковые:
private void OnOneAtATimeTimerElapsed(object state)
{
if (!RestrictOneAtATime())
{
return;
}
//get real busy for two seconds or more
if(!ReleaseOneAtATime())
{
//Well, Hell's bells and buckets of blood!
}
}
bool OneAtATimeInProgress = false;
private bool RestrictToOneAtATime()
{
bool result = false;
if (OneAtATimeLocker.TryEnterWriteLock(0))
{
if(!OneAtATimeInProgress)
{
OneAtATimeInProgress = true;
result = true;
}
OneAtATimeLocker.ExitWriteLock();
}
return result;
}
private bool ReleaseOneAtATime()
{
bool result = false;
//there shouldn't be any 'trying' about it...
if (OneAtATimeLocker.TryEnterWriteLock(0))
{
if(OneAtATimeInProgress)
{
OneAtATimeInProgress = false;
result = true;
}
OneAtATimeLocker.ExitWriteLock();
}
return result;
}
Первый имеет какие-либо последствия для производительности, потому что он фиксирует степень метода?
Второй даже предлагает безопасность, которая, как можно подумать, делает - что-то ускользает от меня?
Есть ли другие способы сделать это надежно и желательно?
2 ответа
Множество способов справиться с этим. Простой способ - просто не делать периодичность таймера, сделать его одним выстрелом, только установив аргумент dueTime. Затем повторно включите таймер в обратном вызове в блоке finally. Это гарантирует, что обратный вызов не может выполняться одновременно.
Это, конечно, делает интервал переменным по времени выполнения обратного вызова. Если это нежелательно, и обратный вызов только иногда занимает больше времени, чем период таймера, тогда простая блокировка выполнит работу. Еще одна стратегия - Monitor.TryEnter и просто отказываются от обратного вызова, если он возвращает false. Ни один из них не особенно хорош, выберите то, что вам больше нравится.
Немного другой рифф на той же концепции, если не нужно обрабатывать каждый тик. Это просто отключает текущий тик таймера, если вы все еще обрабатываете предыдущий тик.
private int reentrancyCount = 0;
...
OnTick()
{
try
{
if (Interlocked.Increment(ref reentrancyCount) > 1)
return;
// Do stuff
}
finally
{
Interlocked.Decrement(ref reentrancyCount);
}
}
Это действительно зависит от того, нужно ли обрабатывать каждый тик. Если вы просто хотите периодически обновлять некоторые данные, чтобы отражать текущее состояние вещей, то вы, вероятно, в порядке, отбрасывая тики, возникающие во время обработки предыдущего тика. Если, с другой стороны, вы должны обработать каждый тик, у вас есть проблема, потому что в этих случаях вам обычно приходится обрабатывать каждый тик своевременно.
Кроме того, если ваш обработчик тиков регулярно занимает больше времени, чем интервал таймера, то либо ваш интервал таймера слишком короткий, либо ваш оптимизатор тиков должен быть оптимизирован. И вы рискуете получить огромное количество задержек для обработки тиков, что означает, что любой статус, который вы обновляете, несколько задерживается.
Если вы решите выбросить перекрывающиеся тики (т.е. вызовы, поступающие во время обработки предыдущего тика), я бы порекомендовал использовать однократный таймер, который сбрасывается каждый раз после обработки. То есть, вместо того, чтобы отбрасывать тики, нельзя допускать совпадения тиков.
Есть ли конкретная причина, по которой вы используете ReaderWriterLockSlim.TryEnterWriteLock
скорее, чем Monitor.TryEnter
?
Кроме того, если вы собираетесь использовать ReaderWriterLockSlim
или же Monitor
, вы должны защитить их try...finally
, То есть используя Monitor
:
if (myLock.TryEnter(0))
{
try
{
// process
}
finally
{
myLock.Exit();
}
}