任务、长时间运行的任务、设备远程操作通信中的线程串行、IVI.NET、Raw-TCP

问题描述

上下文

虽然对于这个问题并不重要,但考虑到一些上下文,这个问题可能会更好地理解(也适用于未来的读者)。

我正在为我的公司开发一个应用程序,用于对不同类型的传感器(压力、温度、流量等)进行自动测量。 此应用程序需要与各种“真实”测量仪器(数字万用表、烤箱、校准器、电源等)进行通信。

仪器将进行通信:

  • 通过使用串行端口的串行通信
  • 使用 Raw-TCP 通过串行网络
  • 通过 GPIB、LAN,使用 IVI 共享驱动程序

所有使用串行通信的仪器都有读取控制字符,它定义了响应的结束(对于串行端口和套接字)

我为每个通过串行端口控制的仪器创建了一个“驱动程序”(即具有属性方法的类,用于通过发送和读取命令和响应来控制仪器),这些仪器具有 Communicator 属性,负责通过正确的接口发送/接收数据。这样我就可以在其他应用中重复使用它。

IVI Shared 兼容设备随附一个 IVI.NET 驱动程序,该驱动程序已经公开了用于控制它们的友好 API。

在我的应用程序中,我定义了一组代表不同类型的接口,如 IDmmICalibrator / ICalibrator<TQuantity>IOven 等,并创建了适配器以使每个驱动程序适应支持的接口。>

该应用程序已经投入生产并“按预期”运行:P 具有同步通信

想法

通信是否应该使用 TPL 异步,即例如具有Task<Quantity> ReadValue() 而不是 Quantity ReadValue() 如果是这样应该如何正确完成?

我的理解

据我所知(如果我正确理解了 TPL):

  • 设备通信是一个阻塞 I/O 绑定的过程。
    • 通过有线/空中发送和接收命令和响应需要一些时间。 在某些情况下,由于不同的原因,它可能需要很长时间。
    • 设备需要计算和回答,这也需要时间。 在某些情况下,由于不同的原因,这可能需要很长时间。(例如,由于多重采样,数字万用表可能需要几秒钟才能进行测量,以及压力校准器读取实际压力)
  • 接收到的计算值(即使用它们进行计算等)也是一个阻塞过程。 在某些情况下,由于不同的原因,这可能需要很长时间。
    • 如果所有工作都发生在 cpu 中,则这只能受 cpu 限制,如果此请求一些外部数据(来自 db、api、文件...),也可以受 I/O 限制
  • 整个测量是 cpu 绑定和 I/O 绑定阻塞过程的混合,也可以看作是单个过程。 这肯定需要一些时间

所以调用堆栈的基本图是:

  • Measurement.Run()(开始测量)调用
    • DeviceAdapter.DoSomething()(创建字符串命令并发送/接收或调用 IVI 驱动程序方法),它调用
      • Communicator.SendCommandReceiveResponse()(通过线路发送和接收)=> 这将是串行端口、套接字或“IVI 驱动程序方式”

当然,在进行测量时,主线程不应在测量期间被阻塞。这项工作应该在“后台”发生,即另一个线程,因此可以同时运行许多测量。

问题

我应该让所有方法都返回 Task 吗?如果是,怎么做?即我应该这样做:

  • await Measurement.Run().ConfigureAwait(false)(开始测量)调用
    • await DeviceAdapter.DoSomething().ConfigureAwait(false)(创建字符串命令并发送/接收或调用 IVI 驱动程序方法),它调用
      • await Communicator.SendCommandReceiveResponse().ConfigureAwait(false)(通过线路发送和接收)=> 这将是串行端口、套接字或“IVI 驱动程序方式”

或者我应该只使通信同步并从我的适配器方法返回任务?

  • await Measurement.Run().ConfigureAwait(false)
    • await DeviceAdapter.DoSomething().ConfigureAwait(false)
      • return Task.Run(Communicator.SendCommandReceiveResponse)

或者我应该让所有测量同步并将其包装在我返回的任务中以及如何?即:

Task Measuement.Run() => Task.Run(RunMeasurement)Task Measurement.Run() => Task.Factory.StartNew(RunMeasurement,TaskCreationoptions.LongRunning) Task.Factory.StartNew 在这里一个不错的选择,因为对于这个长时间运行的过程不会使用线程池线程(?)

void RunMeasurement() => {DeviceAdapter.DoSomething()...}

我应该考虑长时间运行哪些任务(如果声明为一个)?

  • Measuement.Start() 是肯定的
  • DeviceAdapter.DoSomething() 如果它调用方法是长时间运行的,则它会长时间运行。

如果所有异步方式都是正确的路径,那么如果我是对的,我不应该使用 Task.Factory.StartNew 来创建像

这样的测量任务
Task Measurement.Run() => Task.Factory.StartNew(async () =>
{
   await DeviceAdapter.DoSomething();
   ...
   ...
},TaskCreationoptions.LongRunning);

因为它没有意义并且不必要地创建了一个线程,如读取 herehere, 也不要使用类似的东西(这会使在调用者线程中的等待之间运行代码

Task Measurement.Run() 
{
   await DeviceAdapter.DoSomething();
   ...
   ...
};

但使用 Task.Run() 创建任务返回像

Task Measurement.Run() => Task.Run(async () =>
{
   await DeviceAdapter.DoSomething();
   ...
   ...
});

这样,await 块之间的代码将在线程池线程中执行,但(可能长时间运行)await 块中的代码将在非线程池线程中执行(这很好,因为可以在此处创建一个新线程,因为该过程可能需要数百或数千毫秒才能完成)

结论

我希望我能或多或少地表达自己。我确实阅读了很多,但在与外部设备通信时没有找到关于“现代”最佳实践的太多信息,因为我发现的只是“旧的”和同步的。我只是想检查一下我的理解,并认为这可能是一个很好的“示例”,以尝试了解 TPL 中的不同用途以及为未来的读者所提到的沟通最佳实践。

解决方法

我编写的第一个 async/await 应用非常相似:一个管理多个设备的桌面应用,包括实时更新计算(最小值/最大值/stddev/等)。

我建议您暂时忽略长时间运行的任务。现在担心小优化还为时过早。此外,特别是因为您当前的解决方案是同步的,我也会忽略这些计算。让它们在 UI 线程上运行,看看是否足够好。 (我有一个 CalculatedProperties library (docs) 可以帮助实现)。

所以现在只关注 I/O。 I/O 自然是异步的,您应该尽可能使用异步 API。 TCP/IP API 有很好的异步支持;但上次我检查时,C# 中的串行 API 没有很好的异步支持。然后是这个:

IVI Shared 兼容设备随附一个 IVI.NET 驱动程序,该驱动程序已经公开了用于控制它们的友好 API。

我不知道 IVI.NET 驱动程序是否具有良好的异步支持。但是,如果没有,您可以将它包装在 Task.Run 中(注意:这从来都不是理想的,在 ASP.NET 上这不是一个好主意,但对于桌面应用程序来说是可以的)。如果异步支持不足,您可能还需要为串行调用执行此操作。

返回结果后,让 await 带您返回 UI 线程,然后更新所有 VM 值(包括计算)。

您必须注意的一件事是并发请求。当前代码强制序列化,因为它是同步的:发送请求,然后 UI 阻塞等待响应。在响应到来之前,用户甚至不可能尝试发送另一个请求。但是一旦你进入异步状态,用户就可以开始并发行为。例如,用户按下发送请求的按钮,然后在响应到来之前用户按下该设备的另一个按钮。

处理这个问题的正确方法取决于这些命令所代表的内容。我建议首先以一种简单的方式执行此操作:在执行命令时禁用该设备的 UI。一旦你开始工作,然后你可以研究更复杂的解决方案,比如允许排队请求,完成“作业视图”,如果这对你的设备/操作类型有意义。