【问题标题】:C# - periodic data reading and Thread.Sleep()C# - 周期性数据读取和 Thread.Sleep()
【发布时间】:2010-05-28 11:47:57
【问题描述】:

我的 C# 应用程序从特殊的 USB 设备读取数据。数据被读取为所谓的“消息”,每个消息都有 24 个字节。每秒必须读取的消息量可能不同(最大频率相当高,大约每秒 700 条消息),但应用程序必须全部读取。

读取消息的唯一方法是调用函数“ReadMessage”,该函数返回从设备读取的一条消息。该函数来自外部 DLL,我无法修改它。

我的解决方案:我有一个单独的线程,它在程序运行期间一直在运行,唯一的工作就是循环读取消息。然后在主应用程序线程中处理接收到的消息。

“读取线程”中执行的函数如下:

private void ReadingThreadFunction() {
    int cycleCount;
    try {
        while (this.keepReceivingMessages) {
            cycleCount++;
            TRxMsg receivedMessage;
            ReadMessage(devHandle, out receivedMessage);

            //...do something with the message...
        }
    }
    catch {
        //... catch exception if reading failed...
    }
}

此解决方案运行良好,所有消息均已正确接收。但是,应用程序消耗太多资源,我的电脑的 CPU 运行在 80% 以上。因此我想减少它。

感谢“cycleCount”变量,我知道线程的“循环速度”约为每秒 40 000 个循环。这是不必要的太多,因为我需要接收最多 700 条消息/秒。 (并且设备有大约 100 条消息的缓冲区,因此循环速度可以更低一些)

我试图通过 Thread.Sleep(1) 将线程挂起 1 毫秒来降低循环速度;命令。当然,这不起作用,循环速度变成了大约 70 循环/秒,这不足以读取所有消息。我知道这种尝试很愚蠢,让线程进入睡眠状态然后唤醒他需要的时间远超过 1 毫秒。

但是,我不知道还能做什么:除了 Thread.Sleep 之外,还有其他方法可以减慢线程执行速度(以减少 CPU 消耗)吗?还是我完全错了,我应该为这个任务使用不同的东西而不是 Thread,也许是 Threading.Timer 或 ThreadPool?

非常感谢您提出的所有建议。这是我在这里的第一个问题,我是使用线程的初学者,所以如果不够清楚,请原谅。

【问题讨论】:

  • ReadMessage 是阻塞还是非阻塞?
  • 正确的方法是使设备驱动程序中断驱动。我猜硬件制造商在那里失败了。
  • 以下所有基于 1ms sleep 的解决方案请注意系统分辨率一般为 15-16ms,更改请看stackoverflow.com/a/15071477

标签: c# multithreading


【解决方案1】:

使用高精度的计时器。您应该尝试使用多媒体计时器。它将帮助您减少 CPU 使用率。我不确定解决办法,但是对于一个恒定的时间(你不不断改变时间间隔,它是相当准确的)。您可以保持 10 毫秒的分辨率,并且对于计时器的每次调用,您可以读取整个缓冲区,直到它为空或说 100 次。这应该会有所帮助。

【讨论】:

  • System.Timers.Timer 具有 ms 分辨率,以 100-200 次/秒左右的速度读取此内容应该可以解决问题
  • 不,System.Timers.Timer 没有毫秒精度。它下降到每个滴答声大约 16-17 毫秒。我通过反复试验了解到这一点。多媒体定时器有利于提高精度。
  • 我很确定高精度计时器在这里帮不了你。它们非常适合测量事物,但您想释放 CPU,而不是测量事物。我建议在下面实施我的解决方案,因为我相信它是正确的。
  • @SLC - 是的,你是对的,我试过了,它工作得很好......所有消息都收到了,CPU 消耗不再是问题......非常感谢大家你的帮助!
  • @SLC,您上面提供的解决方案既好又简单。但是该解决方案的唯一问题可能是,如果您不断接收消息,则会占用 CPU 进行其他处理。话虽如此,我也知道使用计时器,您可能会进入一个场景,即您已丢弃消息而缓冲区已满。因此,我建议使用 MM 计时器的方法。无论如何,我相信任何适用于场景的方法。如果经过深思熟虑,就是正确的解决方案。
【解决方案2】:

我建议使用分析器,或者使用 VS2010 中的分析器。你需要找出瓶颈,而不是随意改变一些东西来看看它是否会突然起作用。

【讨论】:

  • 我敢打赌分析器会说 90% 的时间都花在了 ReadMessage 方法上。如果您为每 700 条消息调用 40,000 次,这是意料之中的,但知道这将有助于如何?
  • 分析告诉你你认为正在发生的事情是否真的在发生。您可能正在做一些意想不到的事情,或者一些意想不到的事情可能会减慢系统速度。
【解决方案3】:

如果没有消息,ReadMessage 会返回什么?

只有当它告诉你没有消息时,你才应该让线程休眠。在这种情况下只睡 1 毫秒就可以了。

【讨论】:

  • -1:根据当时系统的使用情况,Thread.Sleep(1) 可能会休眠(远)超过 1 毫秒。因此,这可能会导致消息丢失。
【解决方案4】:

使用超时为 1 毫秒的 AutoResetEvent 怎么样?

private void ReadingThreadFunction() {

    try {
            var autoReset = new AutoResetEvent(false);
            autoReset.WaitOne(1);
            TRxMsg receivedMessage;
            ReadMessage(devHandle, out receivedMessage);

            //...do something with the message...
        }
    }
    catch {
        //... catch exception if reading failed...
    }
}

编辑:一定要检查消息是否可用,就像其他答案所建议的那样......

【讨论】:

    【解决方案5】:
    1. 确认您完全理解您的 DLL 公开的 API。有什么方法可以使用库来通知您有新消息的到来?如果您描述的库中没有内置信号或通知,我会感到惊讶。

    2. 如果您必须轮询,那么当您检查消息时,检索所有 条消息。不要只是取回一个然后再去睡觉。获取所有消息,直到没有更多消息可获取,因为您想缓冲以清除。

    【讨论】:

      【解决方案6】:

      我认为你应该做的是使用带计时器的单例类。这个类的作用是每秒给你 700 条消息,如果需要更多,它每秒等待并重试 700 条消息。您可以在另一个线程上实例化它。单例是线程安全的。

      // Queue Start
      ProcessQueue.Instance.Start();
      
      // Queue Stop
      ProcessQueue.Instance.Stop();
      

      例子:

      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Text;
      using System.Timers;
      using System.Diagnostics;
      
      namespace Service.Queue
      {
          /// <summary>
          /// Process Queue Singleton Class
          /// </summary>
          class ProcessQueue : IDisposable
          {
      
              private static readonly ProcessQueue _instance = new ProcessQueue();
              private bool _IsProcessing = false;
              int _maxMessages = 700;
      
              public bool IsProcessing
              {
                  get { return _IsProcessing; }
                  set { _IsProcessing = value; }
              }
      
              ~ProcessQueue()
              {
                  Dispose(false);
              }
      
              public static ProcessQueue Instance
              {
                  get
                  {
                      return _instance;
                  }
              }
      
      
              /// <summary>
              /// Timer for Process Queue
              /// </summary>
              Timer timer = new Timer();
      
              /// <summary>
              /// Starts Process Queue
              /// </summary>
              public void Start()
              {
                  timer.Elapsed += new ElapsedEventHandler(OnProcessEvent);
                  timer.Interval = 1000;
                  timer.Enabled = true;
              }
      
              /// <summary>
              /// Stops Process Queue
              /// </summary>
              public void Stop() {
                  _IsProcessing = false;
                  timer.Enabled = false;
              }
      
              /// <summary>
              /// Process Event Handler
              /// /// </summary>
              private void OnProcessEvent(object source, ElapsedEventArgs e)
              {
                  if (!_IsProcessing)
                  {
                      _IsProcessing = true;
      
              for (int i = 0; i < _maxMessages; i++)
                      {
                  TRxMsg receivedMessage;
                  ReadMessage(devHandle, out receivedMessage);
              }
      
                      _IsProcessing = false;
                  }
      
              }
      
              protected virtual void Dispose(bool disposing)
              {
                  if (disposing)
                  {
                      timer.Dispose();
                  }
              }
      
              #region IDisposable Members
      
              public void Dispose()
              {
                  Dispose(true);
                  GC.SuppressFinalize(this);
              }
      
              #endregion
      
          }
      }
      

      【讨论】:

        【解决方案7】:

        Thread.Sleep 也意味着放弃线程的时间片。只有在再次安排之后,您才会返回到该线程。这就是您“睡眠”时间超过 1 毫秒的原因。另见this post by Peter Ritchie

        问题似乎是您必须主动轮询 DLL 以获取新消息。更好的解决方案是让 DLL 在新消息到达时通知您。但是,您说不能修改外部 DLL。

        您可以尝试减少priority of the polling thread

        编辑:

        例如,您可以使用DispatcherTimer 大约每毫秒触发一次。 DispatcherTimer 不会在事件尚未触发时扰乱您的 CPU。然后,您可以在处理程序中处理消息(我假设您可以判断当前是否没有消息),直到没有消息要处理。您还必须防止重入,即在事件处理程序已经运行时进入它。

        作为代码示例:

        // Creating the DispatcherTimer
        var dispatcherTimer = new System.Windows.Threading.DispatcherTimer();
        dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick);
        dispatcherTimer.Interval = TimeSpan.FromMilliseconds(1);
        dispatcherTimer.Start();
        
        
        private void dispatcherTimer_Tick(object sender, EventArgs e)
        {
            if (this.ProcessingEvents)
                return;
        
            this.ProcessingEvents = true;
        
            var messagesLeft = true;
        
            while (messagesLeft)
            {
                TRxMsg receivedMessage;
                ReadMessage(devHandle, out receivedMessage);
        
                // Process message
                // Set messagesLeft to false if no messages left
            }
        
            this.ProcessingEvents = false;
        }
        

        【讨论】:

          【解决方案8】:

          你可以试试Thread.SpinWait:

          SpinWait 实质上将处理器置于一个非常紧凑的循环中,循环计数由迭代参数指定。因此,等待的持续时间取决于处理器的速度。

          将此与 Sleep 方法进行对比。即使指定的时间间隔为零,调用 Sleep 的线程也会产生其当前处理器时间片的剩余部分。为 Sleep 指定一个非零间隔会从线程调度程序中删除线程,直到时间间隔结束。

          SpinWait 旨在等待不产生当前时间片

          它适用于您知道自己想在很短的时间内做某事的情况,因此失去时间片会过度。

          【讨论】:

          • -1:SpinWait 将在运行时充分利用处理器。这正是 OP 想要避免的。
          【解决方案9】:

          我愿意:

          //...do something with the message... 
          
          // If the message is null, it indicates that we've read everything there is to be read.
          // So lets sleep for 1ms to give the buffer chance to receive a few extra messages.
          If (theMessage == null) // Buffer is empty as we failed to read a new message
          {
              Thread.Sleep(1);
          }
          

          这种方式可能会给你最大的性能,而不是锤击你的 CPU。

          【讨论】:

          • -1:根据当时系统的使用方式,Thread.Sleep(1) 的睡眠时间(远)可能超过 1 毫秒。因此,这可能会导致消息丢失。
          • 错了。缓冲区的大小只有 100 条消息,因此如果我们假设它的 1000 条消息/秒,那就是每毫秒一条消息(实际上是 700 条,所以更加轻松)。这意味着缓冲区填满需要 100 毫秒。睡眠 1ms,即使是 50ms,也不是问题。请记住,它仅在缓冲区为空时暂停,而不是每个周期。此外,任何其他解决方案都将不得不遭受同样的不准确性。无论您选择哪种方法来减慢速度,您都无法准确地得到它。
          • @SLC 你不知道它会暂停多久。直到睡眠返回消息被丢弃可能需要很长时间。在我看来,使用单独的多媒体或系统计时器的解决方案是最好的解决方案。
          • @Daniel 如何阻止计时器/秒表使用 100% cpu?
          • @SLC 计时器在尚未触发时未使用 100% CPU。
          猜你喜欢
          • 2017-11-28
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-12-12
          • 2010-11-19
          • 2013-09-17
          相关资源
          最近更新 更多