解决一个C#中定时任务被阻塞问题

1、前言

本文会介绍一个C#中最简单定时任务的使用方法,以及会遇到的定时任务被阻塞现象,从笔者理解的角度分析原因。以及提供解决方案。

2、C#中定时任务的最简方法.

protected internal void PollClient()
{
    int i=0;
    Timer t = new Timer(p => {
        i++;
        if (deviceContextList.Count > 0)
        {
            var deviceContext=GetDeviceContext("123456789");
                SendMessage(messageList[i%7],deviceContext.tcpSession.writerContext);
            logger.Info("客户端数量:"+ deviceContextList.Count);        
        }
        else 
        {
            logger.Info("客户端数量为0");
            Console.WriteLine("客户端数量为0");
        }              
    }, null, 0, 1000) ;           
}

上面的timer方法提供于微软System.Threading命名空间。System.Threading.Timer 是由线程池调用的。所有的Timer对象只使用了一个线程来管理。这个线程知道下一个回调对象在什么时候到期。

下一个回调对象到期时,线程就会唤醒,在内部调用ThreadPool 的 QueueUserWorkItem,将一个工作项添加到线程池队列中,使你的回调方法得到调用。此方法有多个重载,具体读者可以自行去看。

Timer(TimerCallback callback, object state, int dueTime, int period)

第一个参数callback是回调方法,第二个参数state可以传参给回调方法的参数,第三个参数dueTime是第一次执行回调函数的延时时间,单位毫秒,第四个参数period是调用回调函数的时间间隔。使用起来是不是特别方便,把你需要执行的定时任务放在回调方法中,可独立写成方法,也可像上面一样写成匿名方法的形式。

3、定时任务阻塞现象

当上述任务被执行了几千次以后,定时任务会阻塞,不再执行,也不再打印日志。并且上面的写法有缺陷,。如果回调方法的执行时间很长,计时器可能(在上个回调还没有完成的时候)再次触发。这可能造成多个线程池线程同时执行你的回调方法。并且线程切换也会造成诸多损耗时间。

4、阻塞现象原因分析

上面的方法中使用局部变量来创建指向一个线程定时器。因为局部变量会被GC回收,导致定时器失效。具体改进如下:

static int i=0;
static Timer _timer = null;
protected  void PollClient()
{
     _timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite) ;           
}
private void TimerCallback(object state)
{
        try
        {
            i++;
          
            if (deviceContextList.Count > 0)
            {
                var deviceContext = GetDeviceContext("123456789");
                SendMessage(messageList[i % 7], deviceContext.tcpSession.writerContext);
                logger.Info("客户端数量:" + deviceContextList.Count + "循环次数:" + i);

            }
            else
            {
                logger.Info("客户端数量为0" + "循环次数:" + i);
                Console.WriteLine("客户端数量为0" + "循环次数:" + i);
            }
        }
        catch (Exception e)
        {
            logger.Error("定时测试下发报文异常:" + e);
        }
        finally
        {
            _timer.Change( 1000, Timeout.Infinite);
        }
}

将定时器与计数变量设置为static是为了定时器不被GC回收。定时任务执行完成之后再设置下次调用时间间隔是为了该任务不过多占用线程池中的线程,节省线程切换时间等。

5、问题解决

可以看到任务已经被执行了86665次,优化后不再被GC回收。

解决一个C#中定时任务被阻塞问题