【问题标题】:Non-reentrant C# timer不可重入 C# 计时器
【发布时间】:2010-06-06 19:45:29
【问题描述】:

我正在尝试每隔t 时间调用一个方法f(),但如果之前对f() 的调用尚未完成,请等到它完成。

我已经阅读了一些关于可用计时器的信息,但找不到任何做我想做的事情的好方法,除了手动编写它。任何有关如何实现这一点的帮助将不胜感激,尽管我担心我可能无法找到使用计时器的简单解决方案。

澄清一下,如果t 是一秒,而f() 运行我在下面写的任意持续时间,那么:

Step  Operation    Time taken
1     wait         1s
2     f()          0.6s
3     wait         0.4s (because f already took 0.6 seconds)
4     f()          10s
5     wait         0s (we're late)
6     f()          0.3s
7     wait         0.7s (we can disregard the debt from step 4)

请注意,此计时器的性质是 f() 在重新进入时不需要安全,这里大小为 1 的线程池就足够了。

【问题讨论】:

  • 链接已失效。
  • @astrowalker 谢谢!暂时将其删除,因为这里没有必要。

标签: c# multithreading timer


【解决方案1】:

使用 System.Threading.Timer。用一段时间 Timeout.Infinite 对其进行初始化,使其像一次性计时器一样工作。当 f() 完成后,调用它的 Change() 方法再次为其充电。

【讨论】:

  • 您的意思是设置周期为Timeout.Infinite
【解决方案2】:

如果f() 已经在运行,您可以只使用“全局”级别的变量(或者更可能是与f() 属于同一类的公共属性)。

所以如果f() 在一个名为TimedEvent 的类中,那么f() 要做的第一件事就是设置Running true

这样你的计时器每秒触发一次,然后启动定时事件(如果它尚未运行)
if (!timedEvent.Running) timedEvent.f()

您评论说f() 如果花费的时间超过计时器间隔,则不会立即重复。这是一个公平的观点。我可能会在f() 中包含类似的逻辑,以便Running 保持真实。所以它看起来像这样:

public void f(int t) // t is interval in seconds
{
   this.running = true;

   Stopwatch stopWatch = new Stopwatch();
   stopWatch.Start();

   do
   {
       stopwatch.Reset();

       // Do work here

   } while (stopWatch.Elapsed.Seconds > t); // repeat if f() took longer than t

   this.running = false;   
}

【讨论】:

  • 如果您这样做,请注意竞争条件。
  • 是的,但提问者确实声明它不需要是多线程安全的
  • 非常优雅,虽然这意味着第二个 f 不会立即跟随 10.5 秒 f,不是吗?它们之间会等待 0.5 秒...
  • @PaulG 提问者表示他不想花时间使f 线程安全。 f 的提问者的意思对应于程序的“//在这里工作”部分,而不是程序中的 f。即使提问者的f 不是线程安全的,您也应该确保您的f 正常工作。然而,事实是你的程序本身不是线程安全的,即使提问者的f 是线程安全的。
  • @PaulG 即使设置running volatile 也不会使您的代码正确。考虑您的f 在执行this.running = true 之前被抢占的可能性。
【解决方案3】:

您可以使用非重启定时器,然后在方法完成后手动重启定时器。

请注意,这将导致与您要求的时间有些不同。 (调用之间总会有t时间间隔)

您可以通过将时间间隔设置为lastTick + t - Now 来解决这个问题,如果是<= 0,则立即运行该方法。

如果您需要停止计时器,请注意竞争条件。

【讨论】:

    【解决方案4】:

    您无法让计时器按预定的时间间隔给您打电话。所有计时器所做的就是不早于请求的时间给你回电。

    某些计时器比其他计时器更好(例如,与 System.Threading.Timer 相比,Windows.Forms.Timer 非常不稳定且不可靠)

    要停止重新调用计时器,一种方法是在方法运行时停止计时器。 (根据您使用的计时器类型,您可以在处理程序退出时停止它并重新启动它,或者使用某些计时器,您可以请求单个回调而不是重复回调,因此处理程序的每次执行只需将下一次调用排入队列) .

    为了在这些调用之间保持相对均匀的时间,您可以记录自处理程序上次执行以来的时间,并使用该时间来计算延迟,直到需要下一个事件。例如如果您希望每秒被调用一次并且您的计时器在 1.02 秒完成处理,那么您可以设置下一个计时器回调的持续时间为 0.98 秒,以适应您已经“用完”下一个部分的事实处理过程中的第二个。

    【讨论】:

      【解决方案5】:

      一个简单的解决方案:

      private class Worker : IDisposable
      {
          private readonly TimeSpan _interval;
          private WorkerContext _workerContext;
      
          private sealed class WorkerContext
          {
              private readonly ManualResetEvent _evExit;
              private readonly Thread _thread;
              private readonly TimeSpan _interval;
      
              public WorkerContext(ParameterizedThreadStart threadProc, TimeSpan interval)
              {
                  _evExit = new ManualResetEvent(false);
                  _thread = new Thread(threadProc);
                  _interval = interval;
              }
      
              public ManualResetEvent ExitEvent
              {
                  get { return _evExit; }
              }
      
              public TimeSpan Interval
              {
                  get { return _interval; }
              }
      
              public void Run()
              {
                  _thread.Start(this);
              }
      
              public void Stop()
              {
                  _evExit.Set();
              }
      
              public void StopAndWait()
              {
                  _evExit.Set();
                  _thread.Join();
              }
          }
      
          ~Worker()
          {
              Stop();
          }
      
          public Worker(TimeSpan interval)
          {
              _interval = interval;
          }
      
          public TimeSpan Interval
          {
              get { return _interval; }
          }
      
          private void DoWork()
          {
              /* do your work here */
          }
      
          public void Start()
          {
              var context = new WorkerContext(WorkThreadProc, _interval);
              if(Interlocked.CompareExchange<WorkerContext>(ref _workerContext, context, null) == null)
              {
                  context.Run();
              }
              else
              {
                  context.ExitEvent.Close();
                  throw new InvalidOperationException("Working alredy.");
              }
          }
      
          public void Stop()
          {
              var context = Interlocked.Exchange<WorkerContext>(ref _workerContext, null);
              if(context != null)
              {
                  context.Stop();
              }
          }
      
          private void WorkThreadProc(object p)
          {
              var context = (WorkerContext)p;
              // you can use whatever time-measurement mechanism you want
              var sw = new System.Diagnostics.Stopwatch();
              int sleep = (int)context.Interval.TotalMilliseconds;
              while(true)
              {
                  if(context.ExitEvent.WaitOne(sleep)) break;
      
                  sw.Reset();
                  sw.Start();
      
                  DoWork();
      
                  sw.Stop();
      
                  var time = sw.Elapsed;
                  if(time < _interval)
                      sleep = (int)(_interval - time).TotalMilliseconds;
                  else
                      sleep = 0;
              }
              context.ExitEvent.Close();
          }
      
          public void Dispose()
          {
              Stop();
              GC.SuppressFinalize(this);
          }
      }
      

      【讨论】:

        【解决方案6】:

        如何使用方法 f() 的委托,将它们排队到堆栈中,并在每个委托完成时弹出堆栈?当然,你仍然需要计时器。

        【讨论】:

          【解决方案7】:

          一个简单的线程是实现这一目标的最简单方法。您仍然无法确定您是否在需要时“准确”地调用了它,但它应该很接近......您还可以决定是否要跳过 应该 发生的调用或尝试捕获的调用备份... 这是用于创建线程的简单帮助程序。

          public static Thread StartTimer(TimeSpan interval, Func<bool> operation)
          {
              Thread t = new Thread(new ThreadStart(
                  delegate()
                  {
                      DateTime when = DateTime.Now;
                      TimeSpan wait = interval;
                      while (true)
                      {
                          Thread.Sleep(wait);
                          if (!operation())
                              return;
                          DateTime dt = DateTime.Now;
                          when += interval;
                          while (when < dt)
                              when += interval;
                          wait = when - dt;
                      }
                  }
              ));
              t.IsBackground = true;
              t.Start();
              return t;
          }
          

          【讨论】:

            【解决方案8】:

            为了那些在这里寻找“重新进入”的人的利益:(我知道这对于最初的问题可能为时已晚) 如果有人不反对使用已经提供此类功能的开源库,我已经通过使用Quartz.NET 的实现成功地实现了这一点 当您创建作业并附加触发器时,您可以指定如果先前的触发器尚未完成执行它的作业应该做什么

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2020-06-20
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多