如何安排一个C# Windows服务每天执行任务?

88

我有一个用C#(.NET 1.1)编写的服务,希望在每天午夜执行一些清理操作。由于必须将所有代码包含在服务中,因此最简单的方法是什么?使用Thread.Sleep()和检查时间是否已转变?


10
您真的想要创建一个在内存中运行整天并在午夜执行某些操作的.NET进程吗?为什么您不能在安装工具时设置系统事件,在午夜(或其他可配置的时间)触发该事件,启动您的应用程序,进行所需的操作,然后退出? - Robert P
6
如果我发现有一个占用20-40兆内存、一直运行但除了每天执行一次什么也不做的托管服务,我会有点恼怒的。 :) - Robert P
它是一个重复的链接 - csa
10个回答

87

我不会使用Thread.Sleep()。要么使用一个定时任务(正如其他人已经提到的),要么在你的服务内部设置一个计时器,定期触发(例如每10分钟一次),并检查自上次运行以来日期是否已更改:

private Timer _timer;
private DateTime _lastRun = DateTime.Now.AddDays(-1);

protected override void OnStart(string[] args)
{
    _timer = new Timer(10 * 60 * 1000); // every 10 minutes
    _timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);
    _timer.Start();
    //...
}


private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    // ignore the time, just compare the date
    if (_lastRun.Date < DateTime.Now.Date)
    {
        // stop the timer while we are running the cleanup task
        _timer.Stop();
        //
        // do cleanup stuff
        //
        _lastRun = DateTime.Now;
        _timer.Start();
    }
}

28
把定时器设定为在午夜的时候到期,这样岂不更合理?而不是每分钟都醒来检查时间。 - Kibbee
11
如果服务因任何原因在午夜时未运行,那么你可能希望在服务再次运行时立即执行任务。 - M4N
4
你应该在服务启动时添加一些代码,以找出是否应立即运行,因为服务没有运行,但应该运行。你不应该连续每10分钟进行检查。在启动时检查是否应该运行,如果不是,则找出下一次运行的时间,并设置所需的计时器时间。 - Kibbee
3
@Kibbee:是的,我同意(我们正在讨论的只是一个细节问题)。但是即使定时器每10秒触发一次,也不会有任何害处(即不会占用太多时间)。 - M4N
1
好的解决方案,但上面的代码缺少 _timer.start() 和 _lastRun 应该设置为昨天,像这样:private DateTime _lastRun = DateTime.Now.AddDays(-1); - Zulu Z
显示剩余2条评论

77

看看Quartz.NET。你可以在Windows服务中使用它。它允许您根据配置的计划运行作业,甚至支持简单的“cron job”语法。我在这方面取得了很多成功。

这是其使用的快速示例:

// Instantiate the Quartz.NET scheduler
var schedulerFactory = new StdSchedulerFactory();
var scheduler = schedulerFactory.GetScheduler();

// Instantiate the JobDetail object passing in the type of your
// custom job class. Your class merely needs to implement a simple
// interface with a single method called "Execute".
var job = new JobDetail("job1", "group1", typeof(MyJobClass));

// Instantiate a trigger using the basic cron syntax.
// This tells it to run at 1AM every Monday - Friday.
var trigger = new CronTrigger(
    "trigger1", "group1", "job1", "group1", "0 0 1 ? * MON-FRI");

// Add the job to the scheduler
scheduler.AddJob(job, true);
scheduler.ScheduleJob(trigger);

2
一个好的建议 - 一旦你做的事情比基本的计时器更复杂,你应该看看Quartz。 - serg10
2
Quartz非常易于使用,我认为即使是超级简单的任务,也可以直接使用它。它将提供轻松调整计划的能力。 - Andy White
超级简单不是吗?大多数新手都会在这里遇到问题:http://stackoverflow.com/questions/24628372/quartz-job-scheduler-in-windows-service - Emil
我使用Quartz创建了调度程序,但在部署到Windows服务器时无法正常工作... - vijay sahu

22

每日任务?听起来似乎只需要一个计划任务(控制面板)- 这里不需要服务。


16

它必须是实际的服务吗?你可以只使用Windows控制面板中内置的定期任务吗?


1
将这样一个狭窄目的的服务转换为控制台应用程序应该很容易。 - Stephen Martin
7
他已经说过:“我必须将所有的代码都包含在服务内。” 我认为没有必要质疑最初的需求。这取决于具体情况,但有时使用服务可能比定时任务更加合适,原因也很充分。 - jeremcc
3
+1:因为你始终需要从各个角度来看待你的问题。 - GvS
这取决于您需要管理多少后台任务。我的当前项目有大约9个不同的作业,在各种时间表下执行,并且我使用Quartz.NET在单个Windows服务中运行它们所有来进行调度。 - jeremcc
6
如果我真的每天只有一个任务要运行,那么使用预定任务运行简单的控制台应用程序就可以了。我的观点是一切都取决于具体情况,说“不要使用Windows服务”在我看来并不是一个有用的回答。 - jeremcc
显示剩余2条评论

3
我使用计时器来实现这个功能。
运行一个服务器计时器,每60秒检查一次小时/分钟。
如果是正确的小时/分钟,就运行你的进程。
我将其抽象成一个基类,称为OnceADayRunner。
让我整理一下代码,然后在这里发布。
    private void OnceADayRunnerTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
        using (NDC.Push(GetType().Name))
        {
            try
            {
                log.DebugFormat("Checking if it's time to process at: {0}", e.SignalTime);
                log.DebugFormat("IsTestMode: {0}", IsTestMode);

                if ((e.SignalTime.Minute == MinuteToCheck && e.SignalTime.Hour == HourToCheck) || IsTestMode)
                {
                    log.InfoFormat("Processing at: Hour = {0} - Minute = {1}", e.SignalTime.Hour, e.SignalTime.Minute);
                    OnceADayTimer.Enabled = false;
                    OnceADayMethod();
                    OnceADayTimer.Enabled = true;

                    IsTestMode = false;
                }
                else
                {
                    log.DebugFormat("Not correct time at: Hour = {0} - Minute = {1}", e.SignalTime.Hour, e.SignalTime.Minute);
                }
            }
            catch (Exception ex)
            {
                OnceADayTimer.Enabled = true;
                log.Error(ex.ToString());
            }

            OnceADayTimer.Start();
        }
    }

该方法的核心在于 e.SignalTime.Minute/Hour 的检查。
这里有一些测试钩子等等,但这是使您的经过时间计时器正常工作的样子。

由于服务器繁忙导致轻微延迟可能会导致其错过小时+分钟。 - Jon B
1
当我执行此操作时,我会检查:现在的时间是否大于00:00?如果是,距离上次运行作业是否已经超过24小时?如果是,运行作业,否则再次等待。 - ZombieSheep

2

正如其他人所写的,定时器是您所描述情境中最好的选择。

根据您的确切要求,每分钟检查当前时间可能并不必要。如果您只需要在午夜之后的一小时内执行操作,而不是在午夜准确地执行操作,则可以采用马丁的方法仅检查日期是否发生变化。

如果您想在午夜时执行操作的原因是您希望计算机工作负载较低,请注意:同样的假设通常也会被其他人做出,并且突然间就有100个清理操作在0:00至0:01 a.m.之间启动。

在这种情况下,您应该考虑在不同的时间开始清理。我通常不是在整点进行这些操作,而是在半点(1.30 a.m.是我个人的首选)进行操作。


1
我建议您使用定时器,但将其设置为每45秒检查一次而不是每分钟。否则,在负载较重的情况下,可能会错过特定分钟的检查,因为在定时器触发和您的代码运行并检查当前时间之间,您可能已经错过了目标分钟。

如果您没有检查以确保它没有运行过,那么如果您每45秒检查一次,有可能会出现两次00:00。 - Michael Meadows
好的点子。在检查过程结束时调用Sleep(15000)可以避免这个问题。(或者可能是16000毫秒,以保险起见 ;-) - Treb
我同意,无论您选择什么计时器持续时间,都应该检查它是否已经运行。 - GWLlosa

0

0
对于那些发现上述解决方案无效的人,可能是因为你的类中有一个this,这意味着一个扩展方法,而正如错误消息所说,它只适用于非泛型静态类。你的类不是静态的。这似乎不是一种作为扩展方法有意义的东西,因为它是针对实例进行操作的,所以请删除this

-2

试试这个:

public partial class Service : ServiceBase
{
    private Timer timer;
    public Service()
    {
        InitializeComponent();
    }

    protected override void OnStart(string[] args)
    {
        SetTimer();
    }

    private void SetTimer()
    {
        if (timer == null)
        {
            timer = new Timer();
            timer.AutoReset = true;
            timer.Interval = 60000 * Convert.ToDouble(ConfigurationManager.AppSettings["IntervalMinutes"]);
            timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
            timer.Start();
        }
    }

    private void timer_Elapsed(object source, System.Timers.ElapsedEventArgs e)
    {
        //Do some thing logic here
    }

    protected override void OnStop()
    {
        // disposed all service objects
    }
}

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