Context
Although not really important for the question, this question may be better to understand (also for future readers) given some context.
I am developing an application for my company for making automated measurements of different types of sensors (pressure, temperature, flow, etc.). This application needs to communicate with a wide range of "real" measuring instruments (DMM, oven, calibrators, power supply, etc).
The instruments will communicate:
- via serial communication using Serial Port
- via serial-over-lan using Raw-TCP
- via GPIB, LAN, using IVI Shared Drivers
All instruments using serial communication have read control chars, which define the end of a response (for serial port and socket)
I have created a "driver" (i.e. class with properties and methods for controlling the instrument by sending and reading the commands and responses) for each instrument controlled via Serial Port which have a Communicator
property who is responsable to send/receive data through the correct interface. This way I can reuse it in other apps.
The IVI Shared compliant devices are shipped with an IVI.NET Driver which already exposes a friendly API for controlling them.
In my application I defined a set of interfaces represanting the different types like IDmm
, ICalibrator / ICalibrator<TQuantity>
, IOven
etc. and created adapters to adapt every driver to the supported interfaces.
The app already is in production and works "as expected" :P with synchronous communication
Idea
Should the communication be asynchronous using TPL, i.e having for ex. Task<Quantity> ReadValue()
instead of Quantity ReadValue()
and if so how should this be done correctly?
My understanding
As I understand (if I've understood TPL correctly):
- Device Communication is a blocking I/O bound procedure.
- Sending and receiving the commands and responses through the wire/air will take some time. In some cases and for different causes it can take a very long time.
- Devices need to compute and answer which also takes time. In some cases and for different causes it can take a very long time. (for ex. a DMM can take several seconds to make a measurement due to multisampling, as well as pressure calibrator to read the actual pressure)
- Computing values when received (i.e. making calculations with them etc.) is also a blocking procedure. In some cases and for different causes it can take a very long time.
- This can be only CPU bound if all work happens in the CPU or also I/O bound if this request some external data (from db, api, file...)
- The whole measuremement is then a mix of CPU bound & I/O bound blocking procedures, which also can be seen as a single procedure. This will for sure take some time
So a basic diagramm of the call stack is:
Measurement.Run()
(Start measurement) which callsDeviceAdapter.DoSomething()
(Create string command and send/receive or call IVI Driver method(s)) which callsCommunicator.SendCommandReceiveResponse()
(sends and receives over the wire) => this will be a serial port, socket or the "IVI Driver way"
Of course when doing a measurement the main thread should not be blocked for the time of the measurement. This work should happen "in the background" i.e another thread, so many measurements can be run at the same time.
Question(s)
Should I make all methods return Task and if so how? I.e should I do:
await Measurement.Run().ConfigureAwait(false)
(Start measurement) which callsawait DeviceAdapter.DoSomething().ConfigureAwait(false)
(Create string command and send/receive or call IVI Driver method(s)) which callsawait Communicator.SendCommandReceiveResponse().ConfigureAwait(false)
(sends and receives over the wire) => this will be a serial port, socket or the "IVI Driver way"
or should I make only the communication synchronous and return tasks from my adapters methods?
await Measurement.Run().ConfigureAwait(false)
await DeviceAdapter.DoSomething().ConfigureAwait(false)
return Task.Run(Communicator.SendCommandReceiveResponse)
or should I make all the measurement synchronous and wrapp it in a task which I return and how? I.e:
Task Measuement.Run() => Task.Run(RunMeasurement)
or Task Measurement.Run() => Task.Factory.StartNew(RunMeasurement, TaskCreationOptions.LongRunning)
Task.Factory.StartNew
is a good choice here, since will not use a thread-pool thread for this long running procedure(?)
void RunMeasurement() => {DeviceAdapter.DoSomething()...}
Which tasks (if declare as one) should I consider long running?
Measuement.Start()
is one for sureDeviceAdapter.DoSomething()
will be long running if the method(s) it calls is/are long-running.
If all async way is the correct path, then if I am right I should not use Task.Factory.StartNew
to create the measurement task like
Task Measurement.Run() => Task.Factory.StartNew(async () =>
{
await DeviceAdapter.DoSomething();
...
...
}, TaskCreationOptions.LongRunning);
since it makes no sense and creates a thread needlessly as read here and here, nor not use anything like (this would make to run code between awaits in caller thread)
Task Measurement.Run()
{
await DeviceAdapter.DoSomething();
...
...
};
but use Task.Run()
to create the task to return like
Task Measurement.Run() => Task.Run(async () =>
{
await DeviceAdapter.DoSomething();
...
...
});
This way the code between await blocks will be executed in threadpool-thread but the (maybe long running) the code in the await blocks will be executed in a non-threadpool-thread (which is good, since it is fine to spin a new thread here as the procedure will likely take a few hundred or thousands of ms to complete)
Conclusion
I hope I could express myself more or less. I did read a lot but did not find much info about "modern" best practices when communicating with external devices, since all I found is "old" and synchronous. I just wanted to check my understanding and think this could be a good "example" to try to understand the different uses in the TPL as well as the mentioned communication best practices for future readers.