Потоковое исполнение с использованием System.Threading.Timer и Monitor

Использование System.Threading.Timer приводит к тому, что потоки откручиваются от ThreadPool, что означает, что если интервал выполнения для таймера истекает, когда поток все еще обрабатывается по порядку предыдущего запроса, то тот же обратный вызов будет делегирован для выполнения в другом потоке. Очевидно, что это вызовет проблемы в большинстве случаев, если обратный вызов не будет воспринят повторно, но мне интересно, как это сделать лучше всего (безопасно).

Скажем, мы имеем следующее:

ReaderWriterLockSlim OneAtATimeLocker = new ReaderWriterLockSlim();

OneAtATimeCallback = new TimerCallback(OnOneAtATimeTimerElapsed);
OneAtATimeTimer = new Timer(OneAtATimeCallback , null, 0, 1000);

Если весь shebang быть заблокирован, как таковой:

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 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;
}

Имеются ли в первом случае какие-либо последствия для производительности, поскольку он блокирует размер метода?

Может ли вторая предложить безопасность, которую она могла бы подумать, - что-то ускользает от меня?

Есть ли другие способы сделать это надежно и желательно?

Ответ 1

Много способов справиться с этим. Простым способом является просто не делать таймер периодическим, сделать его одним выстрелом, только установив аргумент dueTime. Затем снова включите таймер в обратном вызове в блоке finally. Это гарантирует, что обратный вызов не может работать одновременно.

Это, конечно, делает переменную интервала временем выполнения обратного вызова. Если это нежелательно, и обратный вызов только изредка занимает больше времени, чем таймер, тогда простая блокировка будет выполнена. Еще одна стратегия - Monitor.TryEnter и просто отказаться от обратного вызова, если он возвращает false. Ни один из них не является особенно превосходным, выберите то, что вам больше нравится.

Ответ 2

Это действительно зависит от того, нужно ли обрабатывать каждый тик. Если вы просто хотите периодически обновлять некоторые данные, чтобы отразить текущее состояние вещей, то вы, вероятно, можете отказаться от тиков, которые происходят во время обработки предыдущего тика. Если, с другой стороны, вы должны обрабатывать каждый тик, тогда у вас есть проблема, потому что в этих случаях вам обычно приходится обрабатывать каждый тик вовремя.

Кроме того, если ваш обработчик меток регулярно занимает больше времени, чем интервал таймера, то ваш тайм-интервал слишком короткий или ваш обработчик тика должен быть оптимизирован. И вы рискуете иметь огромное количество тиков для обработки, что означает, что любой статус, который вы обновляете, несколько задерживается.

Если вы решите выбросить перекрывающиеся тики (т.е. вызовы, входящие во время обработки предыдущего тика), я бы рекомендовал использовать таймер с одним выстрелом, который вы reset каждый раз после обработки. То есть, вместо того, чтобы отбрасывать тики, невозможно сделать перекрывающиеся тики.

Есть ли конкретная причина, по которой вы используете ReaderWriterLockSlim.TryEnterWriteLock, а не Monitor.TryEnter?

Кроме того, если вы собираетесь использовать ReaderWriterLockSlim или Monitor, вы должны защитить их с помощью try...finally. То есть, используя Monitor:

if (myLock.TryEnter(0))
{
    try
    {
        // process
    }
    finally
    {
        myLock.Exit();
    }
}