用.NET编写的闹钟应用程序

12

我并不是真的在写一个闹钟应用程序,但它将有助于阐明我的问题。

假设我的应用程序中有一个方法,我想让这个方法每小时都在整点时钟调用(例如晚上7:00、8:00、9:00等)。我可以创建一个计时器并将其间隔设置为3600000,但最终它会与系统时钟失去同步。或者我可以使用while()循环和Thread.Sleep(n)定期检查系统时间,并在达到所需时间时调用该方法,但我也不喜欢这种方法(Thread.Sleep(n)对我来说是非常糟糕的代码气味)。

我正在寻找的是一种在.Net中让我传入一个未来的DateTime对象和一个方法委托或事件处理程序的方法,但我还没有找到这样的东西。我怀疑Win32 API中有一个可以实现这个功能的方法,但我也找不到那个方法。


4
你是否想要自己制作Quartz.Net? http://quartznet.sourceforge.net/ - Austin Salonen
1
@Austin:不,我只是想要一个简单的方法,就像我在最后一段描述的那样,而不必自己编写或使用某些第三方组件。 - MusiGenesis
1
内置的Windows计划程序具有可通过PInvoke访问的API http://www.pinvoke.net/default.aspx/netapi32/NetScheduleJobAdd.html - xcud
@xcud:NetScheduleJobAdd接近我所需要的,但它执行的是一个命令字符串而不是调用一个方法。 - MusiGenesis
7个回答

14

或者,您可以创建一个间隔为1秒的计时器,并每秒检查一次当前时间,直到达到事件时间,如果是这样,就会触发您的事件。

您可以为此编写一个简单的包装器:

public class AlarmClock
{
    public AlarmClock(DateTime alarmTime)
    {
        this.alarmTime = alarmTime;

        timer = new Timer();
        timer.Elapsed += timer_Elapsed;
        timer.Interval = 1000;
        timer.Start();

        enabled = true;
    }

    void  timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        if(enabled && DateTime.Now > alarmTime)
        {
            enabled = false;
            OnAlarm();
            timer.Stop();
        }
    }

    protected virtual void OnAlarm()
    {
        if(alarmEvent != null)
            alarmEvent(this, EventArgs.Empty);
    }


    public event EventHandler Alarm
    {
        add { alarmEvent += value; }
        remove { alarmEvent -= value; }
    }

    private EventHandler alarmEvent;
    private Timer timer;
    private DateTime alarmTime;
    private bool enabled;
}

使用方法:

AlarmClock clock = new AlarmClock(someFutureTime);
clock.Alarm += (sender, e) => MessageBox.Show("Wake up!");

请注意上面的代码非常简略,且不是线程安全的。


是的,但那也不是我要找的,抱歉。 - MusiGenesis
4
你所寻找的并不存在。 - RichardOD
1
抱歉,你在寻找什么呢? - Coincoin
1
我觉得MusiGenesis真的需要一个作业调度器。 - RichardOD
4
你可以计算时间间隔而不是每秒检查。 - user159335
显示剩余2条评论

8

有趣的是,我曾经遇到过非常类似的问题,并在.Net框架中寻找处理这种情况的方法。最终,我们实现了自己的解决方案,这是一种while循环的变体,其中包含Thread.Sleep(n),其中n越接近目标时间就越小(实际上是对数级别的,但具有合理的阈值,以便在接近目标时间时不会使CPU占用过高)。下面是一个非常简单的实现,它只会休眠从当前时间到目标时间之间一半的时间。

class Program
{
    static void Main(string[] args)
    {
        SleepToTarget Temp = new SleepToTarget(DateTime.Now.AddSeconds(30),Done);
        Temp.Start();
        Console.ReadLine();
    }

    static void Done()
    {
        Console.WriteLine("Done");
    }
}

class SleepToTarget
{
    private DateTime TargetTime;
    private Action MyAction;
    private const int MinSleepMilliseconds = 250;

    public SleepToTarget(DateTime TargetTime,Action MyAction)
    {
        this.TargetTime = TargetTime;
        this.MyAction = MyAction;
    }

    public void Start()
    {
        new Thread(new ThreadStart(ProcessTimer)).Start();
    }

    private void ProcessTimer()
    {
        DateTime Now = DateTime.Now;

        while (Now < TargetTime)
        {
            int SleepMilliseconds = (int) Math.Round((TargetTime - Now).TotalMilliseconds / 2);
            Console.WriteLine(SleepMilliseconds);
            Thread.Sleep(SleepMilliseconds > MinSleepMilliseconds ? SleepMilliseconds : MinSleepMilliseconds);
            Now = DateTime.Now;
        }

        MyAction();
    }
}

2
您可以每次触发计时器时简单地重置计时器的持续时间,代码如下:
// using System.Timers;

private void myMethod()
{
    var timer = new Timer { 
        AutoReset = false, Interval = getMillisecondsToNextAlarm() };
    timer.Elapsed += (src, args) =>
    {
        // Do timer handling here.

        timer.Interval = getMillisecondsToNextAlarm();
        timer.Start();
    };
    timer.Start();
}

private double getMillisecondsToNextAlarm()
{
    // This is an example of making the alarm go off at every "o'clock"
    var now = DateTime.Now;
    var inOneHour = now.AddHours(1.0);
    var roundedNextHour = new DateTime(
        inOneHour.Year, inOneHour.Month, inOneHour.Day, inOneHour.Hour, 0, 0);
    return (roundedNextHour - now).TotalMilliseconds;
}

不幸的是,这并不能解决问题。漂移问题是由于计时器在长时间内不准确(秒表也有同样的问题)。 - MusiGenesis
我以前从未听说过这个。你基于什么来宣称这是不准确的呢? - Jacob
@Jacob:不要只听我的话 - 亲自试一试。启动一个计时器(任何类型),让它运行一两天,看看它会偏差多少。 - MusiGenesis
我不明白这个“漂移”问题。我在服务中使用了一个间隔时间为五秒的System.Timers.Timer,并且在我使用它的四个月内没有出现任何漂移。我知道这是事实,因为该服务在每个间隔上写入事件日志,我每五秒钟就像钟表一样有事件发生,这是预期的。 - AMissico
@AMissico:您的服务只启动了计时器一次,然后在每个已过时间时写入事件日志,还是您的服务定期由Windows重新启动?我刚刚再次测试了一下,我的计时器(间隔为5000)在仅12分钟后就与系统时间相差了整整1秒。 - MusiGenesis

1
你可以创建一个Alarm类,该类具有专用线程,该线程休眠直到指定的时间,但这将使用Thread.Sleep方法。代码示例如下:
/// <summary>
/// Alarm Class
/// </summary>
public class Alarm
{
    private TimeSpan wakeupTime;

    public Alarm(TimeSpan WakeUpTime)
    {
        this.wakeupTime = WakeUpTime;
        System.Threading.Thread t = new System.Threading.Thread(TimerThread) { IsBackground = true, Name = "Alarm" };
        t.Start();
    }

    /// <summary>
    /// Alarm Event
    /// </summary>
    public event EventHandler AlarmEvent = delegate { };

    private void TimerThread()
    {
        DateTime nextWakeUp = DateTime.Today + wakeupTime;
        if (nextWakeUp < DateTime.Now) nextWakeUp = nextWakeUp.AddDays(1.0);

        while (true)
        {
            TimeSpan ts = nextWakeUp.Subtract(DateTime.Now);

            System.Threading.Thread.Sleep((int)ts.TotalMilliseconds);

            try { AlarmEvent(this, EventArgs.Empty); }
            catch { }

            nextWakeUp = nextWakeUp.AddDays(1.0);
        }
    }
}

1
确实。更像是你可以做的原型。最好使用像Jacob描述的计时器。 - JDunkerley

1
我知道这是一个有些老旧的问题,但当我在寻找另一个答案时,我偶然发现了它。由于我最近遇到了这个特定问题,所以我想在这里提供我的建议。
另一件你可以做的事情就是像这样安排方法的时间表:
/// Schedule the given action for the given time.
public async void ScheduleAction ( Action action , DateTime ExecutionTime )
{
    try
    {
        await Task.Delay ( ( int ) ExecutionTime.Subtract ( DateTime.Now ).TotalMilliseconds );
        action ( );
    }
    catch ( Exception )
    {
        // Something went wrong
    }
}


考虑到它只能等待 int 32 的最大值(大约一个月左右),它应该适合您的需求。使用方法:
void MethodToRun ( )
{
    Console.WriteLine ("Hello, World!");
}

void CallingMethod ( )
{
    var NextRunTime = DateTime.Now.AddHours(1);
    ScheduleAction ( MethodToRun, NextRunTime );
}

而且你应该在一个小时内收到控制台消息。


-1

不,那和我要找的东西完全不一样。 - MusiGenesis

-2

我以前使用过这个,取得了巨大的成功:

Vb.net:

Imports System.Threading
Public Class AlarmClock
    Public startTime As Integer = TimeOfDay.Hour
    Public interval As Integer = 1
    Public Event SoundAlarm()
    Public Sub CheckTime()
        While TimeOfDay.Hour < startTime + interval
            Application.DoEvents()
        End While
        RaiseEvent SoundAlarm()
    End Sub
    Public Sub StartClock()
        Dim clockthread As Thread = New Thread(AddressOf CheckTime)
        clockthread.Start()
    End Sub
End Class

C#:

using System.Threading;
public class AlarmClock
{
    public int startTime = TimeOfDay.Hour;
    public int interval = 1;
    public event SoundAlarmEventHandler SoundAlarm;
    public delegate void SoundAlarmEventHandler();
    public void CheckTime()
    {
        while (TimeOfDay.Hour < startTime + interval) {
            Application.DoEvents();
        }
        if (SoundAlarm != null) {
            SoundAlarm();
        }
    }
    public void StartClock()
    {
        Thread clockthread = new Thread(CheckTime);
        clockthread.Start();
    }
}

我不知道C#是否可行,但VB运行得很好。

在VB中的使用:

Dim clock As New AlarmClock
clock.interval = 1 'Interval is in hours, could easily convert to anything else
clock.StartClock()

然后,只需要为SoundAlarm事件添加一个事件处理程序。

1
TimeOfDay是仅限于VB.Net的东西,而带有DoEvents()调用的While循环将在此过程运行时使您的处理器达到最大值。 - MusiGenesis
以前不知道,但现在我知道了。原始代码中循环休眠15秒,但由于您不需要,所以我将其删除了。 - Cyclone
为什么你想要在运行线程的方法中使用Application.DoEvents(或者完全使用它)是超出我理解范围的。 - Matt Wilko

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