使用System.Threading.Timer和Monitor进行线程安全执行

时间:2011-02-26 00:42:40

标签: c# .net thread-safety

使用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'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;
}

第一个是否有任何性能影响,因为它会锁定方法的范围?

第二个甚至提供人们可能认为的安全性 - 这是逃避我的事吗?

还有其他方法可靠地解决这个问题吗?

3 个答案:

答案 0 :(得分:10)

有很多方法可以解决这个问题。一种简单的方法是不要使定时器周期性,只需设置 dueTime 参数即可使其成为一次。然后在finally块中的回调中重新启用计时器。这可以保证回调不能同时运行。

这当然使得区间可以通过回调的执行时间来变量。如果这是不可取的并且回调仅偶尔花费比计时器周期更长的时间,那么简单的锁定将完成工作。另一个策略是Monitor.TryEnter,如果它返回false,就放弃回调。这些都不是特别优越,选择你最喜欢的。

答案 1 :(得分:0)

这实际上取决于你是否需要处理每个滴答。如果您只是想定期更新某些数据以反映事物的当前状态,那么您可能可以放弃在处理上一个滴答时发生的滴答。另一方面,如果你必须处理每个滴答,那么你就会遇到问题,因为在这些情况下你通常必须及时处理每个滴答。

此外,如果您的tick处理程序通常需要的时间超过计时器间隔,那么您的计时器间隔时间太短或者需要优化您的tick处理程序。而且你有可能需要处理大量的滴答声,这意味着你所更新的状态会有所延迟。

如果您决定丢弃重叠的刻度(即在处理上一个刻度时进入的调用),我建议您使用每次处理后重置的一次性计时器。也就是说,不是丢弃蜱,而是不可能发生重叠蜱。

您使用ReaderWriterLockSlim.TryEnterWriteLock而不是Monitor.TryEnter是否有特殊原因?

此外,如果您要使用ReaderWriterLockSlimMonitor,则应使用try...finally保护它们。也就是说,使用Monitor

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

答案 2 :(得分:0)

如果您不必处理所有滴答声,则对同一概念的重复处理稍有不同。如果您仍在处理上一个滴答,则仅基于当前计时器滴答。

private int reentrancyCount = 0;
...
OnTick()
{
    try
    {
        if (Interlocked.Increment(ref reentrancyCount) > 1)
            return;

        // Do stuff
    }
    finally
    {
        Interlocked.Decrement(ref reentrancyCount);
    }
}