C# - 定期读取数据和 Thread.Sleep()

3
我的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是阻塞还是非阻塞的? - SwDevMan81
正确的方法是使设备驱动程序中断驱动的。我想硬件制造商在这方面失败了。 - Sedat Kapanoglu
所有基于1毫秒休眠的解决方案都应注意系统分辨率通常为15-16毫秒,如需更改,请参阅https://dev59.com/ZGUp5IYBdhLWcg3wxpit#15071477。 - Andreas Reiff
9个回答

2

使用高精度的计时器。尝试使用多媒体计时器。这将有助于保持CPU使用率较低。我不确定分辨率,但对于一个常数时间(您不改变时间间隔的情况下),它相当准确。您可以保持10毫秒的分辨率,并且在每次计时器调用时,可以读取整个缓冲区,直到它为空或者说100次为止。这应该会有所帮助。


System.Timers.Timer 具有毫秒级分辨率,每秒读取约 100-200 次应该可以为您解决问题。 - jishi
不,System.Timers.Timer没有毫秒级精度。它大约每个tick下降到16-17ms左右。我通过试错学到了这一点。多媒体定时器适用于更高的精度。 - Carlos
我非常确定高精度计时器在这里不会有帮助。它们适用于测量事物,但您想要释放CPU,而不是测量某些东西。我建议实施下面的解决方案,因为我相信这是正确的。 - NibblyPig
@SLC - 是的,你说得对,我尝试了一下,它可以很好地工作... 所有的消息都被接收了,CPU 消耗也不再是一个问题... 非常感谢大家的帮助! - Ondra C.
@SLC,你提供的解决方案很好也很简单。但是这种解决方案唯一的问题可能是如果你不断地接收消息,就会占用CPU而无法进行其他处理。话虽如此,我也知道使用定时器可能会出现缓冲区满了而丢失消息的情况。因此,我建议采用MM定时器的方法。总之,我相信任何对场景有效的解决方案都是正确的;只要经过深思熟虑,就是正确的解决方案。 - Sudesh Sawant
@Sudesh 除非你的计算机处理消息太慢,否则它最终会清空缓冲区并线程休眠,这样就不会占用CPU。 - NibblyPig

0

Thread.Sleep的意思是放弃线程的时间片。只有在它再次被调度后,你才会返回到该线程。这就是为什么你会“睡眠”超过1毫秒的原因。另请参见Peter Ritchie的这篇文章

问题似乎在于你必须主动轮询DLL以获取新消息。更好的解决方案是当新消息到达时,DLL通知你。然而,你说你无法修改外部DLL。

你可以尝试降低轮询线程的优先级

编辑:

你可以例如使用一个 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;
}

0

使用一个超时为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...
    }
}

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


0

我认为你应该使用一个带有定时器的单例类。这个类可以每秒提供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

    }
}

0
我建议你使用一个分析器,或者使用VS2010中的分析器。你需要找出瓶颈所在,而不是随意更改一些东西,看看是否突然可以工作。

我敢打赌,分析器会说90%的时间都花在了ReadMessage方法上。如果你为每700个消息调用它40000次,那么这是可以预料的,但知道这一点有什么帮助呢? - Cobusve
性能分析告诉你你认为正在发生的事情是否确实正在发生。你可能正在做一些意外的操作,或者一些意外事件可能导致系统变慢。 - Carlos

0

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

如果线程告诉您没有消息,则只应让线程休眠。在这种情况下,仅睡眠1ms即可解决问题。


-1:根据系统当前的使用情况,Thread.Sleep(1) 可能会睡眠(远)超过1毫秒。因此这可能导致消息丢失。 - Daniel Rose

0
1. 确认您完全理解 DLL 公开的 API。有没有使用库来通知您新消息到达的方法?如果像您所描述的这样的库没有内置的信号或通知,那会让人感到惊讶。 2. 如果必须轮询,则在检查消息时检索 所有 消息。不要只检索一个消息然后再次休眠。获取所有消息,直到没有更多消息可获取,因为您希望缓冲区清除。

-1

您可以尝试使用Thread.SpinWait

SpinWait本质上将处理器置于非常紧密的循环中,循环次数由iterations参数指定。因此,等待的持续时间取决于处理器的速度。

与Sleep方法相比,有所不同。调用Sleep的线程会放弃其当前的处理器时间片,即使指定的间隔为零。为Sleep指定非零间隔会将线程从线程调度程序的考虑范围中移除,直到时间间隔过去。

SpinWait旨在等待而不放弃当前的时间片

它适用于您知道需要在非常短的时间内执行某些操作的情况,因此失去时间片将是过度的情况。


-1: SpinWait会在运行时充分利用处理器。这正是OP想要避免的。 - Daniel Rose

-2

我会这样做:

//...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毫秒。因此可能导致消息丢失。 - Daniel Rose
错误。缓冲区仅能容纳100条消息,因此假设每秒有1000条消息,那么每毫秒只有一条消息(实际上是700,所以更为轻松)。这意味着需要100毫秒才能填满缓冲区。即使睡眠1毫秒,即使是50毫秒,也不是问题。请记住,它只在缓冲区为空时暂停,而不是每个周期都暂停。此外,任何其他解决方案都将遭受相同的不准确性。无论您选择哪种方法来减慢速度,您永远无法得到精确的结果。 - NibblyPig
@SLC 你无法确定它会暂停多长时间。可能会一直等到睡眠返回,导致消息丢失。使用单独的多媒体或系统定时器解决方案似乎是最好的解决方案。 - Daniel Rose
@Daniel 你怎样让定时器/秒表停止占用100%的CPU呢? - NibblyPig
@SLC 当计时器尚未触发时,它们不会使用100%的CPU。 - Daniel Rose
显示剩余3条评论

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