问题描述
上下文
虽然对于这个问题并不重要,但考虑到一些上下文,这个问题可能会更好地理解(也适用于未来的读者)。
我正在为我的公司开发一个应用程序,用于对不同类型的传感器(压力、温度、流量等)进行自动测量。 此应用程序需要与各种“真实”测量仪器(数字万用表、烤箱、校准器、电源等)进行通信。
仪器将进行通信:
- 通过使用串行端口的串行通信
- 使用 Raw-TCP 通过串行网络
- 通过 GPIB、LAN,使用 IVI 共享驱动程序
所有使用串行通信的仪器都有读取控制字符,它定义了响应的结束(对于串行端口和套接字)
我为每个通过串行端口控制的仪器创建了一个“驱动程序”(即具有属性和方法的类,用于通过发送和读取命令和响应来控制仪器),这些仪器具有 Communicator
属性,负责通过正确的接口发送/接收数据。这样我就可以在其他应用中重复使用它。
IVI Shared 兼容设备随附一个 IVI.NET 驱动程序,该驱动程序已经公开了用于控制它们的友好 API。
在我的应用程序中,我定义了一组代表不同类型的接口,如 IDmm
、ICalibrator / ICalibrator<TQuantity>
、IOven
等,并创建了适配器以使每个驱动程序适应支持的接口。>
该应用程序已经投入生产并“按预期”运行:P 具有同步通信
想法
通信是否应该使用 TPL 异步,即例如具有Task<Quantity> ReadValue()
而不是 Quantity ReadValue()
如果是这样应该如何正确完成?
我的理解
据我所知(如果我正确理解了 TPL):
- 设备通信是一个阻塞 I/O 绑定的过程。
- 通过有线/空中发送和接收命令和响应需要一些时间。 在某些情况下,由于不同的原因,它可能需要很长时间。
- 设备需要计算和回答,这也需要时间。 在某些情况下,由于不同的原因,这可能需要很长时间。(例如,由于多重采样,数字万用表可能需要几秒钟才能进行测量,以及压力校准器读取实际压力)
- 接收到的计算值(即使用它们进行计算等)也是一个阻塞过程。 在某些情况下,由于不同的原因,这可能需要很长时间。
- 整个测量是 cpu 绑定和 I/O 绑定阻塞过程的混合,也可以看作是单个过程。 这肯定需要一些时间
所以调用堆栈的基本图是:
-
Measurement.Run()
(开始测量)调用
当然,在进行测量时,主线程不应在测量期间被阻塞。这项工作应该在“后台”发生,即另一个线程,因此可以同时运行许多测量。
问题
我应该让所有方法都返回 Task 吗?如果是,怎么做?即我应该这样做:
或者我应该只使通信同步并从我的适配器方法返回任务?
-
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()...}
我应该考虑长时间运行哪些任务(如果声明为一个)?
如果所有异步方式都是正确的路径,那么如果我是对的,我不应该使用 Task.Factory.StartNew
来创建像
Task Measurement.Run() => Task.Factory.StartNew(async () =>
{
await DeviceAdapter.DoSomething();
...
...
},TaskCreationoptions.LongRunning);
因为它没有意义并且不必要地创建了一个线程,如读取 here 和 here, 也不要使用类似的东西(这会使在调用者线程中的等待之间运行代码)
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。一旦你开始工作,然后你可以研究更复杂的解决方案,比如允许排队请求,完成“作业视图”,如果这对你的设备/操作类型有意义。