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

8
使用 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
或者你可以在进入tick事件处理程序时停止计时器...并在退出时重新启用... - Mitch Wheat
我希望计时器可以继续滴答作响以进行审计,但感谢您的努力。 - Grant Thomas
您想让计时器一直滴答作响以便它不能工作吗?这是一个奇怪的要求! - Mitch Wheat
3个回答

13

有很多方法可以解决这个问题。一种简单的方法是将定时器设置为一次性,只设置dueTime参数,然后在回调函数中使用finally块重新启用定时器。这可以确保回调函数不会并发运行。

当然,这会使间隔时间因回调执行时间而变量化。如果这不可取,而且回调函数只偶尔需要比定时器周期更长的时间,则简单的锁定就能完成任务。另一种策略是使用Monitor.TryEnter,如果返回false则放弃回调函数。这些方法都没有特别优越的地方,选择自己最喜欢的即可。


我不确定你的意思,我从未提到停止计时器。使用Monitor.TryEnter()可以让你记录重叠。 - Hans Passant
抱歉,我是在指你的“一次性定时器”建议。 - Grant Thomas
顺便问一下,有没有任何建议会对性能产生明显的影响? - Grant Thomas
不,获取锁并不昂贵。大约几十纳秒左右。 - Hans Passant
对于一次性的 System.Threading.Timer,由于每个调用可能/将会是不同的线程,是否仍需要进行锁定? - markmnl
显示剩余2条评论

1
如果您不必处理每个时钟周期,那么这个概念有所不同。如果您仍在处理先前的时钟周期,则此方法会简单地放弃当前的计时器周期。
private int reentrancyCount = 0;
...
OnTick()
{
    try
    {
        if (Interlocked.Increment(ref reentrancyCount) > 1)
            return;

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

0

这真的取决于您是否必须处理每个时刻。如果您只想定期更新一些数据以反映事物的当前状态,那么在处理上一个时刻时发生的时刻可以被丢弃,您可能会没问题。另一方面,如果您必须处理每个时刻,则存在问题,因为在这些情况下通常必须及时处理每个时刻。

此外,如果您的时刻处理程序经常需要比计时器间隔更长的时间,则计时器间隔太短或需要优化时刻处理程序。并且您面临的风险是有大量待处理的时刻积压,这意味着您正在更新的任何状态都会有所延迟。

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

您使用ReaderWriterLockSlim.TryEnterWriteLock而不是Monitor.TryEnter的特定原因吗?

此外,如果你要使用ReaderWriterLockSlim或者Monitor,应该用try...finally保护它们。也就是说,当使用Monitor时:
if (myLock.TryEnter(0))
{
    try
    {
        // process
    }
    finally
    {
        myLock.Exit();
    }
}

谢谢您提供这些信息,但我在其他评论中提到,我实际上需要审计这一事实的发生;但同时,我不想排队请求 - 它们可以被丢弃,只是记录一下。但是 0 表示我期望立即获得锁,因此它们不会排队,也许 -1 会无限期等待。 - Grant Thomas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接