C# - 计时器滴答时间比指定的间隔更短,导致重复作业并行运行

Posted

技术标签:

【中文标题】C# - 计时器滴答时间比指定的间隔更短,导致重复作业并行运行【英文标题】:C# - Timer ticks shorter than specified in the interval causing duplicate jobs to run in parallel 【发布时间】:2020-02-15 04:58:30 【问题描述】:

我有一个每 4 分钟打勾的 Windows 服务。如果运行 DataImporter,则当计时器计时,DataImporter 有许多“作业”,它可以在每个计时运行,例如,有一个 ProcessData 作业和一个 RetreiveData 作业:

RetreiveData 作业将访问第 3 方 API 并将数据存储在 DB 中以供处理。 ProcessData 作业将从数据库中获取数据并将其处理到我们可用的数据库等中。

一旦 DataImporter 运行,它就会检查一个名为 ScheduledJob 的数据库表 - 它具有许多调度功能,例如 FrequencyInterval、ActiveStart/Stop 时间、StartedLastRun 时间。 ScheduledJob 表有一个名为“InProgress”的标志,这个标志将停止 DataImport 在它已经运行时拾取该作业。

存在一个连续的问题,即一个作业被拾取两次,彼此相隔几秒钟,然后两者同时运行,这会在尝试插入相同的记录时导致许多数据库限制。我不太确定它如何同时选择两个工作,滴答声相隔 4 分钟,所以理论上它甚至不能查看潜在的工作,它怎么能同时运行它们相隔几秒?

RetrieveData 和 ProcessData 作业都需要能够并行运行,因此我无法在执行作业时暂停 Timer。

服务:

public partial class DataImport : ServiceBase

    private int _eventId = 0;
    readonly Timer _serviceTimer = new Timer(240000);

    public DataImport()
    
        InitializeComponent();
        ImportServiceEventLog.Source = ServiceSource.DATA_IMPORT_SERVICE.ToString() + Global.ReleaseModeSource(); ;
    

    protected override void OnStart(string[] args)
    
        ImportServiceEventLog.WriteEntry(ServiceSource.DATA_IMPORT_SERVICE.ToString() + Global.ReleaseModeSource() + " started", EventLogEntryType.Information, _eventId++);
        _serviceTimer.AutoReset = true;
        ImportServiceEventLog.WriteEntry(ServiceSource.DATA_IMPORT_SERVICE.ToString() + Global.ReleaseModeSource() + " timer interval = " + _serviceTimer.Interval / 1000 + " seconds", EventLogEntryType.Information, _eventId++);
        _serviceTimer.Elapsed += new ElapsedEventHandler(OnTimer);
        _serviceTimer.Start();
    

    protected override void OnStop()
    
        ImportServiceEventLog.WriteEntry(ServiceSource.DATA_IMPORT_SERVICE.ToString() + Global.ReleaseModeSource() + " stopped", EventLogEntryType.Information, _eventId++);
    

    public void OnTimer(object sender, ElapsedEventArgs args)
    
        try
        
            Run();
        
        catch (System.Exception ex)
        
            ImportServiceEventLog.WriteEntry(ServiceSource.DATA_IMPORT_SERVICE.ToString() + Global.ReleaseModeSource() + " error: " + ex.ToString(), EventLogEntryType.Information, _eventId++);
        
    

    public void Run()
    
        using (var dataImportController = new DataImportController())
        
            dataImportController.Run();
                        
    

数据导入控制器:

public class DataImportController

    public void Run()
    
        // Gets all the jobs from the ScheduledJob table in the DB
        var jobs = GetJobsToRun();

        //Get all Processes (from DB)
        foreach (var job in jobs)
        
            //Check the time it was last run - do this for each process
            if (RunJob(job))
            
                _messaging.EventMessage("Process " + job.Name + " finished : " + DateTime.Now, ServiceSource.DATA_IMPORT_SERVICE);
            
        
    

    public bool RunJob(ScheduledJob job)
    
        // Checks if the job is ready to run, i.e. is the InProgress flag set to false and the interval long enough since the StartedLastRun DateTime
        if (!job.IsReadyToRun())
        
            return false;
        

        // Set job to in progress
        job.InProgress = true;
        job.StartedLastRun = DateTime.Now;
        _scheduledJobRepository.Update(job);
        _scheduledJobRepository.SaveChanges();

        try
        
            switch (job.Name.ToUpper())
            
                case "RetreiveData":
                    // RUN JOB
                    break;
                case "ProcessData":
                    // RUN JOB
                    break;                    
            

            job.InProgress = false;
            job.EndedLastRun = DateTime.Now;
            _scheduledJobRepository.Update(job);
            _scheduledJobRepository.SaveChanges();
        
        catch (Exception exception)
        
            _messaging.ReportError("Error occured whilst checking we are ready to run " + exception.Message, exception, null, 0, ServiceSource.DATA_IMPORT_SERVICE);
        

        return true;
       

编辑:

包含 Program.cs

static void Main()

    if (!Environment.UserInteractive)
    
        ServiceBase[] ServicesToRun;
        ServicesToRun = new ServiceBase[]
        
        new DataImport()
        ;
        ServiceBase.Run(ServicesToRun);
    

【问题讨论】:

您的服务是否可能停止,然后重新启动?如果OnStart 被调用两次,它会在计时器中添加另一个处理程序,所以每次调用OnTimer 都会快速连续调用两次。但是,它应该等到一个 OnTimer 调用完成后再再次调用它。 不,日志中没有任何内容表明它已停止和启动。 您是否有可能得到两个DataImport 实例?例如。它在 ServiceBase.Run? 中指定了两次? 不,不要以为会发生这种情况,我已经包含 Program.cs 文件来向您展示它是如何被调用的。 是什么让你认为这份工作被接了两次,相隔几秒钟?这是来自日志记录,还是你从其他东西推断出来的? GetJobsToRun 是否有可能两次返回同一个工作? 【参考方案1】:

尝试在 OnTimer 函数中停止计时器,然后在完成任务后重新启动计时器。

【讨论】:

"RetrieveData 和 ProcessData 作业都需要能够并行运行,因此我无法在执行作业时暂停 Timer。"【参考方案2】:

如果担心重叠,请放弃计时器并使用 Task.Delay 进行异步循环:

async Task SomeFunc(CancellationToken token)

    while(!token.IsCancellationRequested)
    
        DoWork();
        await Task.Delay(timeInterval, token);
    

【讨论】:

我已经实现了,将不得不等待,看看它是否有效。谢谢。 在调用 DoWork() 时暂停执行是否存在问题? @cullimorer 确实如此。您可以通过从下一个间隔中减去执行工作所花费的时间来缓解这种情况。如果小于0,则立即再去。【参考方案3】:

您在OnStart 订阅了定时器事件,并且没有在OnStop 取消订阅。

_serviceTimer.Elapsed += new ElapsedEventHandler(OnTimer); 和 AutoReset 的初始化移动到构造函数。在OnStop 中停止计时器。那应该可以解决您的问题。我相信您的服务已多次启动(重新启动)。

【讨论】:

这个在cmets里讨论过,OP说他们的服务没有启动两次 这可能会在不经意间发生,这取决于他如何重新安装/调试服务。我建议在编译和安装带有建议更改的新版本后重新启动 PC。 他们已经使用他们现有的日志进行了检查,这不是原因

以上是关于C# - 计时器滴答时间比指定的间隔更短,导致重复作业并行运行的主要内容,如果未能解决你的问题,请参考以下文章

捕获倒计时或滴答事件 - Threading.Timer C#

c#中Timer是单线程还是多线程?

STM32怎么用库函数使用滴答定时器?

linux系统的最小时间间隔是多少

C# 如何间隔一定的时间执行一次代码?

使用倒数计时器以特定间隔发出信号