正确的轮询方式?

27

我是一名软硬件工程师,有丰富的C语言和嵌入式技术经验。目前我正忙于编写一些使用硬件进行数据采集的C# (.NET)应用程序。现在,对我来说最紧迫的问题是:

例如: 我有一台机器,它有一个终端开关,用于检测轴的最终位置。现在我正在使用USB数据采集模块读取数据。目前,我正在使用线程来连续读取端口状态。

该设备没有中断功能。

我的问题是:这是正确的方式吗?应该使用定时器,线程还是任务?我知道轮询是大多数人“讨厌”的东西,但欢迎任何建议!


8
为什么我们会讨厌民意调查? - L-Four
有没有像 .OnFinalPositionArrived 这样的事件可以挂钩? - Einer
你应该使用 System.Threading.Timer 来进行轮询 - 这样可以避免创建新线程。 - Matthew Watson
@L-Three:因为当我在寻找最佳实现方式时,我看到了很多关于轮询的不赞成评论。这就是为什么我发表了那个评论的原因 ;) - Velocity
@Einer:遗憾的是,随USB DAQ模块提供的库不支持这样的事件 :( - Velocity
使用条件信号在Pthreads中进行轮询怎么样? - SatKetchum
4个回答

60
我认为这在很大程度上取决于您的具体环境,但首先 - 在大多数情况下,您不应再使用线程。Tasks是更方便和更强大的解决方案。
  • 低轮询频率:计时器+在Tick事件中进行轮询:
    计时器易于处理和停止。不必担心后台运行的线程/任务,但处理过程发生在主线程中

  • 中等轮询频率:Task+ await Task.Delay(delay):
    await Task.Delay(delay)不会阻塞线程池线程,但由于上下文切换,最小延迟为~15ms

  • 高轮询频率:Task+ Thread.Sleep(delay)
    可用于1ms延迟 - 我们实际上是使用此方法来轮询我们的USB测量设备

可以按以下方式实现:

int delay = 1;
var cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;
var listener = Task.Factory.StartNew(() =>
{
    while (true)
    {
        // poll hardware

        Thread.Sleep(delay);
        if (token.IsCancellationRequested)
            break;
    }

    // cleanup, e.g. close connection
}, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

在大多数情况下,您可以使用Task.Run(() => DoWork(), token),但没有重载提供TaskCreationOptions.LongRunning选项,该选项告诉任务调度程序不使用普通线程池线程。
但正如您所看到的,Tasks更易于处理(并且可以await,但这里不适用)。特别是“停止”只需在代码中的任何位置调用cancellationTokenSource.Cancel()

您甚至可以在多个操作中共享此令牌,并立即停止它们。此外,未启动的任务在取消令牌时不会启动。

您还可以将另一个操作附加到任务以在一个任务后运行:

listener.ContinueWith(t => ShutDown(t));

在监听器完成后,这将被执行,您可以进行清理操作(t.Exception包含任务动作的异常,如果不成功)。


环境是带有Windows 8的平板电脑。应用程序是WPF。间隔需要很高,因此您建议使用带有Thread.Sleep(delay)的Task?为什么线程不是正确的解决方案?感谢您提供的信息! - Velocity
太棒了!非常感谢你提供的清晰示例和解释!我之前不知道 Task.Delay 的上下文切换大约需要 15 毫秒!还有一个小问题:为什么在这种情况下直接使用线程不是一个好的解决方案?是因为线程池的原因吗? - Velocity
请查看我的详细解释,但线程本身并不是坏的,但建议在大多数情况下不再使用它们,因为任务的开销很小,但“可用性好处”很大... - Christoph Fink
好的!但从性能技术角度来看:在多核环境下,任务是否比(较低的)线程表现更好?或者它们都被分成片段用于核心? - Velocity
当它们运行时,它们的执行方式相同,因为Task简单地说是一个“封装线程”。任务在启动和停止期间只需一些开销即可完成“所有魔法”... - Christoph Fink
非常感谢您的详细解释!我将用 Task 解决方案替换当前的 Thread 解决方案! - Velocity

3

无法避免IMO轮询。

您可以创建一个模块,带有独立的线程/任务,定期轮询端口。根据数据的变化,此模块将引发事件,由使用应用程序处理。


这也是一个好主意!但模块本身由线程或任务组成,对吧?在更改后,您会触发一个事件? - Velocity
抱歉,你已经提出了这个建议!我忽略了你的答案!;) - Velocity
我个人喜欢使用Tasks(http://blog.slaks.net/2013-10-11/threads-vs-tasks/),但通常这取决于工作的紧急程度和对库的信心水平。 - Manish Dalal

1

可能是:

   public async Task Poll(Func<bool> condition, TimeSpan timeout, string message = null)
    {
        // https://github.com/dotnet/corefx/blob/3b24c535852d19274362ad3dbc75e932b7d41766/src/Common/src/CoreLib/System/Threading/ReaderWriterLockSlim.cs#L233 
        var timeoutTracker = new TimeoutTracker(timeout);
        while (!condition())
        {
            await Task.Yield();
            if (timeoutTracker.IsExpired)
            {
                if (message != null) throw new TimeoutException(message);
                else throw new TimeoutException();
            }
        }
    }

请查看SpinWaitTask.Delay的内部原理。


0
我一直在思考这个问题,你可以建立一个抽象层来利用Tasks和Func、Action与轮询服务,将Func、Action和轮询间隔作为参数传入。这样可以保持两种功能的实现分离,同时使它们能够注入到轮询服务中。
例如,你可以像这样使用它作为你的轮询类。
public class PollingService {
    public void Poll(Func<bool> func, int interval, string exceptionMessage) {
        while(func.Invoke()){
            Task.Delay(interval)
        }
        throw new PollingException(exceptionMessage)
    }

    public void Poll(Func<bool, T> func, T arg, int interval, string exceptionMessage) 
    {
        while(func.Invoke(arg)){
            Task.Delay(interval)
        }
        throw new PollingException(exceptionMessage)
    }
}

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