【问题标题】:CPU friendly infinite loopCPU 友好的无限循环
【发布时间】:2011-11-16 03:49:12
【问题描述】:

写一个无限循环很简单:

while(true){
    //add whatever break condition here
}

但这会破坏 CPU 性能。这个执行线程将尽可能多地消耗 CPU 的能量。

降低对 CPU 影响的最佳方法是什么? 添加一些Thread.Sleep(n) 应该可以解决问题,但是为Sleep() 方法设置较高的超时值可能表明应用程序对操作系统没有响应。

假设我需要在控制台应用程序中每分钟左右执行一项任务。 我需要保持Main() 在“无限循环”中运行,而计时器将触发完成这项工作的事件。我想保持Main() 对CPU 的影响最小。

你建议什么方法。 Sleep() 可以,但正如我已经提到的,这可能表明操作系统没有响应线程。

稍后编辑:

我想更好地解释我在寻找什么:

  1. 我需要一个控制台应用程序而不是 Windows 服务。控制台应用可以在带有 Compact Framework 的 Windows Mobile 6.x 系统上模拟 Windows 服务。

  2. 只要 Windows Mobile 设备正在运行,我需要一种方法来保持应用程序的运行。

  3. 我们都知道控制台应用程序只要它的静态 Main() 函数运行就会运行,所以我需要一种方法来防止 Main() 函数退出。

  4. 在特殊情况下(比如:更新应用程序),我需要请求应用程序停止,所以我需要无限循环并测试一些退出条件。例如,这就是为什么Console.ReadLine() 对我没用。没有退出条件检查。

  5. 关于上述,我仍然希望 Main() 函数尽可能资源友好。撇开检查退出条件的函数的指纹。

【问题讨论】:

  • Thread.Sleep(0) 可能不错
  • 你为什么不Timer
  • @leppie Sleep(0) 放弃当前时间片的剩余部分,但仍会导致 100% 的 CPU 利用率。所以在这里不起作用。
  • 您能否详细说明“可能表明应用程序对操作系统无响应”的问题?您确定这是控制台应用程序中的问题吗?
  • 对于 SETI 和类似的应用程序,我会简单地降低我的进程/线程的优先级,这样当其他人需要它时它就不会获得 CPU 时间。

标签: c# .net console-application infinite-loop


【解决方案1】:

要避免无限循环,只需使用WaitHandle。要让进程从外部世界退出,请使用带有唯一字符串的EventWaitHandle。下面是一个例子。

如果您是第一次启动它,它会每 10 秒打印一条消息。如果您同时启动程序的第二个实例,它将通知其他进程正常退出并立即退出。此方法的 CPU 使用率:0%

private static void Main(string[] args)
{
    // Create a IPC wait handle with a unique identifier.
    bool createdNew;
    var waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, "CF2D4313-33DE-489D-9721-6AFF69841DEA", out createdNew);
    var signaled = false;

    // If the handle was already there, inform the other process to exit itself.
    // Afterwards we'll also die.
    if (!createdNew)
    {
        Log("Inform other process to stop.");
        waitHandle.Set();
        Log("Informer exited.");

        return;
    }

    // Start a another thread that does something every 10 seconds.
    var timer = new Timer(OnTimerElapsed, null, TimeSpan.Zero, TimeSpan.FromSeconds(10));

    // Wait if someone tells us to die or do every five seconds something else.
    do
    {
        signaled = waitHandle.WaitOne(TimeSpan.FromSeconds(5));
        // ToDo: Something else if desired.
    } while (!signaled);

    // The above loop with an interceptor could also be replaced by an endless waiter
    //waitHandle.WaitOne();

    Log("Got signal to kill myself.");
}

private static void Log(string message)
{
    Console.WriteLine(DateTime.Now + ": " + message);
}

private static void OnTimerElapsed(object state)
{
    Log("Timer elapsed.");
}

【讨论】:

  • 这是一个比这个线程中的大多数人更好的答案,我从使用 while(true) 样式循环的约 30% CPU 使用率变为使用 WaitHandle 的平均约 0.04% 的 CPU 使用率。跨度>
  • 如何按事件退出循环再开始新的?
  • @AntoshaShmonoff:要退出循环,您必须从不同的线程或进程调用 waitHandle.Set(),然后使用 new EventWaitHandle(...) 重新创建句柄并再次执行 while 循环。
  • 我只是不明白如何使用这一行 var timer = new Timer(OnTimerElapsed, null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
  • 它创建了一个System.Threading.Timer。参数是应该调用的委托,方法的参数,等待第一次调用的时间以及所有后续调用之间的时间间隔。
【解决方案2】:

您可以使用System.Threading.Timer 类,它提供了在给定时间段内异步执行回调的能力。

public Timer(
    TimerCallback callback,
    Object state,
    int dueTime,
    int period
)

作为替代方案,System.Timers.Timer 类公开了Elapsed Event,它会在给定的时间段过去后引发。

【讨论】:

  • 时间的最小分辨率是 30 毫秒——这对他来说可能是也可能不是问题;至少在你的回答中注明。
  • @Jonathan Dickinson :我相信从 CPU 负载的角度来看,与 100% CPU 负载相反,这种离散值已经足够了,但无论如何感谢您提供的详细信息
  • @Jonathan Dickinson:顺便说一句,你在哪里找到这个值?
  • 如果您正在执行线程处理,这应该是常识(因为它会影响 一切Environment.TickCountThread.Sleep 等) - 哎呀,它是 15.6 毫秒:stackoverflow.com/questions/3744032/…
  • 是的,但为什么是 30ms 而不是 ~15ms?是否引用了 MSDN?
【解决方案3】:

为什么你会容忍使用无限循环?对于这个例子,将程序设置为计划任务,每分钟运行一次,不是更经济吗?

【讨论】:

  • 我认为每分钟启动一个新的 .net 程序实例的开销有点高。
  • 可能会有更多开销,但我仍然会回避无限循环或服务。您可以两全其美,每 5 分钟运行一次任务,但该任务有一个计时器,它每分钟触发一次动作,持续 5 分钟。这意味着更少的开销,从每分钟启动 .Net 程序开始,内存泄漏的机会也更少,内存泄漏可能会锁定 CPU 并影响其他进程和/或用户。
【解决方案4】:

您为什么不编写一个小应用程序并使用系统的任务调度程序每分钟、每小时...等运行一次?

另一种选择是编写一个在后台运行的 Windows 服务。该服务可以使用 MSDN 上如下所示的简单警报类:

http://msdn.microsoft.com/en-us/library/wkzf914z%28v=VS.90%29.aspx#Y2400

您可以使用它来定期触发您的方法。这个 Alarm 类在内部使用了一个计时器:

http://msdn.microsoft.com/en-us/library/system.timers.timer.aspx

只需正确设置计时器的时间间隔(例如 60000 毫秒),它就会定期引发 Elapsed 事件。将事件处理程序附加到 Elapsed 事件以执行您的任务。无需实现“无限循环”只是为了让应用程序保持活力。这由服务为您处理。

【讨论】:

  • 从表面上看,任务计划程序至少只能每 5 分钟运行一次..
  • 最近更喜欢使用 Quartz.NET 作为调度任务的一种方式。使用 Topshelf 创建 Windows 服务并通过 Quartz.NET 调度作业。
  • 希望我在开始我最近的项目之前看到 Quartz.Net.. 在纸上看起来很棒..
【解决方案5】:

我为一个应用程序执行此操作,该应用程序必须在将文件拖放到文件夹时对其进行处理。你最好的选择是一个计时器(如建议的那样),在“main”的末尾有一个 Console.ReadLine() 而不是循环。

现在,您担心告诉应用停止:

我也通过一些基本的“文件”监视器完成了这项工作。只需在应用程序的根文件夹中创建文件“quit.txt”(通过我的程序或可能要求它停止的其他应用程序)将使应用程序退出。半码:

<do your timer thing here>
watcher = new FileSystemWatcher();
watcher.Path = <path of your application or other known accessible path>;
watcher.Changed += new FileSystemEventHandler(OnNewFile);
Console.ReadLine();

OnNewFile 可能是这样的:

private static void OnNewFile(object source, FileSystemEventArgs e)
{
    if(System.IO.Path.GetFileName(e.FullPath)).ToLower()=="quit.txt")
        ... remove current quit.txt
        Environment.Exit(1);
}

现在您提到这是(或可能是)移动应用程序?您可能没有文件系统观察程序。在那种情况下,也许您只需要“杀死”该过程(您说“在特殊情况下(例如:更新应用程序),我需要请求该应用程序停止”。停止它的“请求者”是谁,应该简单地杀死进程)

【讨论】:

    【解决方案6】:

    在我看来,您希望 Main() 进入可中断循环。为此,必须在某处涉及多个线程(或者您的循环必须定期轮询;但我在这里不讨论该解决方案)。同一应用程序中的另一个线程或另一个进程中的线程必须能够向您的 Main() 循环发出信号,表明它应该终止。

    如果这是真的,那么我认为您想使用 ManualResetEventEventWaitHandle 。您可以等待该事件,直到它发出信号(并且信号必须由另一个线程完成)。

    例如:

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace Demo
    {
        class Program
        {
            static void Main(string[] args)
            {
                startThreadThatSignalsTerminatorAfterSomeTime();
                Console.WriteLine("Waiting for terminator to be signalled.");
                waitForTerminatorToBeSignalled();
                Console.WriteLine("Finished waiting.");
                Console.ReadLine();
            }
    
            private static void waitForTerminatorToBeSignalled()
            {
                _terminator.WaitOne(); // Waits forever, but you can specify a timeout if needed.
            }
    
            private static void startThreadThatSignalsTerminatorAfterSomeTime()
            {
                // Instead of this thread signalling the event, a thread in a completely
                // different process could do so.
    
                Task.Factory.StartNew(() =>
                {
                    Thread.Sleep(5000);
                    _terminator.Set();
                });
            }
    
            // I'm using an EventWaitHandle rather than a ManualResetEvent because that can be named and therefore
            // used by threads in a different process. For intra-process use you can use a ManualResetEvent, which 
            // uses slightly fewer resources and so may be a better choice.
    
            static readonly EventWaitHandle _terminator = new EventWaitHandle(false, EventResetMode.ManualReset, "MyEventName");
        }
    }
    

    【讨论】:

      【解决方案7】:

      您可以使用Begin-/End-Invoke 让步给其他线程。例如

      public static void ExecuteAsyncLoop(Func<bool> loopBody)
      {
          loopBody.BeginInvoke(ExecuteAsyncLoop, loopBody);
      }
      
      private static void ExecuteAsyncLoop(IAsyncResult result)
      {
          var func = ((Func<bool>)result.AsyncState);
          try
          {
              if (!func.EndInvoke(result))
                  return;
          }
          catch
          {
              // Do something with exception.
              return;
          }
      
          func.BeginInvoke(ExecuteAsyncLoop, func);
      }
      

      你会这样使用它:

      ExecuteAsyncLoop(() =>
          {
              // Do something.
              return true; // Loop indefinitely.
          });
      

      这使用了我机器上一个内核的 60%(完全空循环)。或者,您可以在循环体中使用此 (Source) 代码:

      private static readonly bool IsSingleCpuMachine = (Environment.ProcessorCount == 1);
      [DllImport("kernel32", ExactSpelling = true)]
      private static extern void SwitchToThread();
      
      private static void StallThread()
      {
          // On a single-CPU system, spinning does no good
          if (IsSingleCpuMachine) SwitchToThread();
          // Multi-CPU system might be hyper-threaded, let other thread run
          else Thread.SpinWait(1);
      }
      
      while (true)
      {
          // Do something.
          StallThread();
      }
      

      这使用了我机器上一个内核的 20%。

      【讨论】:

      • 我相信你不需要在每个循环周期检查当前机器是否是一个矿石多核,在应用启动时检查一次
      • @sll static readonly with initializer - 这意味着它被检查一次。
      【解决方案8】:

      解释 CodeInChaos 的评论:

      您可以设置给定的thread's priority。线程根据其优先级安排执行。用于确定线程执行顺序的调度算法因每个操作系统而异。所有线程默认为“正常”优先级,但如果您将循环设置为低;它不应该从设置为正常的线程中窃取时间。

      【讨论】:

        【解决方案9】:

        Timer 方法可能是您最好的选择,但既然您提到了 Thread.Sleep,对于类似的问题,有一个有趣的 Thread.SpinWaitSpinWait struct 替代方案,有时比短的 Thread.Sleep 调用更好。

        另请参阅此问题:What's the purpose of Thread.SpinWait method?

        【讨论】:

          【解决方案10】:

          这里有很多“高级”答案,但 IMO 只需使用 Thread.Sleep(lowvalue) 就足够了。

          计时器也是一种解决方案,但计时器背后的代码也是一个无限循环 - 我会假设 - 它会在经过的时间间隔内触发您的代码,但它们具有正确的无限循环设置。

          如果您需要大量睡眠,可以将其分成较小的睡眠。

          因此,对于非 UI 应用来说,这样的事情是一个简单易用的 0% CPU 解决方案。

          static void Main(string[] args)
          {
              bool wait = true;
              int sleepLen = 1 * 60 * 1000; // 1 minute
              while (wait)
              {
          
                  //... your code
          
                  var sleepCount = sleepLen / 100;
                  for (int i = 0; i < sleepCount; i++)
                  {
                      Thread.Sleep(100);
                  }
              }
          }
          

          关于操作系统如何检测应用程序是否无响应。除了 UI 应用程序之外,我不知道有任何其他测试,其中有方法可以检查 UI 线程是否处理 UI 代码。 UI 上的线程休眠很容易被发现。 Windows“应用程序无响应”使用简单的本机方法“SendMessageTimeout”来检测应用程序是否有无响应的 UI。

          UI 应用程序上的任何无限循环都应始终在单独的线程中运行。

          【讨论】:

            【解决方案11】:

            要保持控制台应用程序运行,只需在 Main() 中的代码末尾添加 Console.ReadLine()

            如果用户不应该能够终止应用程序,您可以使用如下循环来执行此操作:

            while (true){
               Console.ReadLine();
            }
            

            【讨论】:

            • 似乎他的应用程序最好没有 UI。顺便说一句,一旦第一次到达 Console.ReadLine() ......应用程序将等待按键,直到它开始下一次迭代。有人必须每分钟按一个键....似乎是迷失的一集。
            • @Peter 我仍然希望对应用程序优雅关闭的时刻进行一些控制。所以循环似乎是一个不错的选择
            • 您如何知道何时退出应用程序?也许您可以在计时器回调中执行此检查。然后你可以通过调用Environment.Exit(0)关闭应用程序。
            猜你喜欢
            • 1970-01-01
            • 2015-08-06
            • 2012-09-25
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-07-17
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多