【问题标题】:Only raise an event if the previous one was completed仅在前一个事件完成时才引发事件
【发布时间】:2014-05-15 16:02:44
【问题描述】:

我在我的应用程序中使用System.Timers.Timer。每一秒我都会运行一个函数来完成一些工作。问题是,这个函数可以阻塞一小段时间(它从磁盘读取然后处理一个大文件)。只有在其先前的“执行实例”完成时,我才想启动该功能。我以为我可以通过Mutex 实现这一点:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        static Mutex TimerMut = new Mutex(false);
        public static void Main()
        {

            Thread TT = new Thread(new ThreadStart(delegate()
            {
                System.Timers.Timer oTimer = new System.Timers.Timer();
                oTimer.Elapsed += new ElapsedEventHandler(Handler);

                oTimer.Interval = 1000;
                oTimer.Enabled = true;
            }));

            TT.Start();

            Console.Read();
        }

        private static void Handler(object oSource,
            ElapsedEventArgs oElapsedEventArgs)
        {
            TimerMut.WaitOne();
            Console.WriteLine("foo");
            Thread.Sleep(500);         //simulate some work
            Console.WriteLine("bar");
            TimerMut.ReleaseMutex();
        }
    }
}

那不行,“foos”仍然每秒出现一次。我怎样才能做到这一点?

编辑:你是对的,启动一个新线程来处理这个是没有意义的。我以为只有System.Threading.Timer 在单独的线程中启动。

【问题讨论】:

  • 我已经编辑了你的标题。请参阅“Should questions include “tags” in their titles?”,其中的共识是“不,他们不应该”。
  • 也许 foo 每秒出现一次,因为你的模拟工作只持续半秒。但是,这种方法的问题在于,如果前一个实例正在运行,则 Handler 需要退出而不做任何事情。您的代码正在等待前一个代码完成,然后继续。如果您的工作需要 20 秒,那么将有 20 次对 Handler 的调用等待互斥体并在前一个退出时继续执行工作。
  • 您的代码甚至不应该编译。您已经在方法中声明了一个静态变量……这意味着您正在运行的代码不是您向我们展示的代码。请制作一个简短但完整的程序来演示该问题。
  • @szczurcio:好的,那么现在你能把它变成一个完整的例子吗?我们应该可以复制、粘贴、编译、运行、观察问题。

标签: c# timer


【解决方案1】:

我不确定您为什么要使用新线程来启动计时器,因为计时器在它们自己的线程上运行,但这里有一个可行的方法。只需关闭计时器,直到完成当前间隔。

static System.Timers.Timer oTimer

public static void Main()
{
    oTimer = new System.Timers.Timer();
    oTimer.Elapsed += new ElapsedEventHandler(Handler);

    oTimer.Interval = 1000;
    oTimer.Enabled = true;                 
}

private void Handler(object oSource, ElapsedEventArgs oElapsedEventArgs)
{
    oTimer.Enabled = false;

    Console.WriteLine("foo");
    Thread.Sleep(5000);         //simulate some work
    Console.WriteLine("bar");

    oTimer.Enabled = true;
}

【讨论】:

  • 这其实是不一样的。下一个刻度将在前一个刻度完成后 1 秒。如果需要 1.5 秒,则意味着第二个滴答声是在开始后 2.5 秒,而不是在开始后 2 秒。
  • @Servy 有时这是期望的行为。如果我是读取大文件的 OP,我会选择这个。
  • @Alb 当然,有各种各样的行为不是问题所要求的,但在其他情况下是可取的。但关键是这并没有解决被问到的问题。我从来没有说这永远不合适,只是它不能解决手头的问题。
  • @Servy 好吧,如果我们逐个字符地回答问题,那么您的解决方案会非常准确地回答一个聪明的(我什至冒着风险:最好的)想法。如果我们接受现实:处理大文件并不是您想要经常做的事情。除此之外,上述解决方案是最易读和最简单的。正如一位好老师曾经说过的那样:“如果你能简单地解决它,为什么要复杂”,我会把你的好解决方案留给对那一秒有严格规范的情况。我什至会在你的回答中提到它,但这只是我的想法。 :)
  • @Alb 解决方案在行为不正确时的可读性或简单程度无关紧要。简单的解决方案肯定优于复杂的解决方案当它们表现相同时
【解决方案2】:

如果你想在另一个已经在工作的情况下跳过勾号,你可以这样做。

private readonly object padlock = new object();

private void SomeMethod()
{
    if(!Monitor.TryEnter(padlock))
        return;

    try
    {
        //Do heavy work
    }
    finally
    {
        Monitor.Exit(padlock);
    }
}

【讨论】:

  • 锁定对于这类工作来说有点过度反应。此外,锁定语法随着时间的推移发生了变化:您应该更新。 :)
  • @Alb 锁定是一个完全合理的考虑,因为该问题需要跨线程同步。虽然 可能 可以避免它,但正如我的回答中所见,喜欢锁并不是不合理的。此外,如果您确实想使用监视器,这是合适的方法,没有比这更好的语法了。
【解决方案3】:

如果此方法的另一个调用仍在运行,您可以按照以下模式跳过指示的工作:

private int isWorking = 0;
public void Foo()
{
    if (Interlocked.Exchange(ref isWorking, 1) == 0)
    {
        try
        {
            //Do work
        }
        finally
        {
            Interlocked.Exchange(ref isWorking, 0);
        }
    }
}

您与Mutex 一起使用的方法将导致额外的滴答声等待较早的滴答声完成,而不是在另一个仍在运行时跳过调用,这就是你说你想要的。 (在处理这样的计时器时,通常希望跳过此类滴答,而不是等待。如果您的滴答处理程序经常花费太长时间,您最终会得到一个等待处理程序的巨大队列。)

【讨论】:

  • 这看起来很复杂。它与if(mutex.WaitOne(0)) { /* do work */; mutes.ReleaseMutex(); } 的作用相同,但更高效 还是什么?
  • @Sinatr 它具有根本不同的语义。如果代码当前正在工作,这将跳过执行代码。您正在描述将等到另一个调用完成然后开始工作的代码。我什至在我的回答中解释了这些选项之间的区别。
  • 不,mutex.WaitOne(0) 会检查,但不会阻止任何内容。如果你得到true(只有一个线程会得到它)——互斥锁是拥有的,你可以工作。我以前没见过你的模式,所以很好奇它是否比使用 Mutex 更好。
【解决方案4】:

我知道做这种事情的最简单方法:

internal static volatile bool isRunning;

public static void Main()
    {
        Thread TT = new Thread(new ThreadStart(delegate()
            {
                System.Timers.Timer oTimer = new System.Timers.Timer();
                oTimer.Elapsed += new ElapsedEventHandler(Handler);

                oTimer.Interval = 1000;
                oTimer.Enabled = true;                 
            }));

        TT.Start();
    }

    private void Handler(object oSource,
        ElapsedEventArgs oElapsedEventArgs)
    {
        if(isRunning) return;

        isRunning = true;

        try
        {
           Console.WriteLine("foo");
           Thread.Sleep(500);         //simulate some work
           Console.WriteLine("bar");
        }
        finally { isRunning = false; }            
    }

处理程序仍在运行,但它所做的第一件事是确保另一个处理程序没有运行,如果有,它会立即停止。

对于更快地执行处理程序的计时器(例如每秒 3-4 次),这有可能竞争;两个线程可以在其中一个设置位之前通过保护子句。您可以通过几个 lock 语句来避免这种情况,类似于 Mutex 或 Monitor:

    static object syncObj = new object();

    private void Handler(object oSource,
        ElapsedEventArgs oElapsedEventArgs)
    {
        lock(syncObj)
        {
           if(isRunning) return;            
           isRunning = true;
        }

        try
        {
           Console.WriteLine("foo");
           Thread.Sleep(500);         //simulate some work
           Console.WriteLine("bar");
        }
        finally { lock(syncObj) { isRunning = false; } }            
    }

这将确保只有一个线程可以检查或修改 isRunning,并且由于 isRunning 被标记为 volatile,CLR 不会将其值缓存为每个线程状态的一部分以提高性能;每个线程必须查看完全相同的内存位置来检查或更改值。

【讨论】:

    猜你喜欢
    • 2022-12-06
    • 1970-01-01
    • 2020-12-23
    • 2017-09-06
    • 1970-01-01
    • 1970-01-01
    • 2011-09-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多