0

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 calls
    • DeviceAdapter.DoSomething() (Create string command and send/receive or call IVI Driver method(s)) which calls
      • Communicator.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 calls
    • await DeviceAdapter.DoSomething().ConfigureAwait(false) (Create string command and send/receive or call IVI Driver method(s)) which calls
      • await 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.StartNewis 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 sure
  • DeviceAdapter.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.

Rafa Gomez
  • 690
  • 8
  • 24
  • Any task which is not CPU-bound and does not block, does not need to be declared long-running. I.e. async IO does not block a thread, the return call is picked up by the thread when ready. Also, if any code up to the first `await` is CPU-bound or blocks then you must put it on another thread with `Task.Run`, otherwise no need. – Charlieface Jan 10 '21 at 13:03

1 Answers1

1

The first async/await app I wrote was very similar: a desktop app that managed multiple devices, including live updates of calculations (min/max/stddev/etc).

I recommend that you ignore long-running tasks for the moment. It's way too early to be worrying about minor optimizations. Also, especially since your current solution is synchronous, I would ignore the calculations as well. Just let them run on the UI thread, and see if that's good enough. (I have a CalculatedProperties library (docs) that may help with the implementation).

So just focus on the I/O for now. I/O is naturally asynchronous, and you should use asynchronous APIs whenever possible. TCP/IP APIs have great asynchronous support; but last time I checked, serial APIs in C# do not have great asynchronous support. And then there's this:

The IVI Shared compliant devices are shipped with an IVI.NET Driver which already exposes a friendly API for controlling them.

I have no idea if the IVI.NET Driver has good asynchronous support. But if it doesn't, you can wrap it in Task.Run (note: this is never ideal, and it's a bad idea on ASP.NET, but it's OK for desktop apps). You may also have to do this for the serial calls if the asynchronous support is insufficient.

As you get the results back, let the await take you back to the UI thread and then update all your VM values (including calculations).

The one thing you do have to be careful of is concurrent requests. The current code forces serialization because it is synchronous: the request is sent and then the UI blocks waiting for a response. It's not even possible for the user to try to send another request until that response comes in. But as soon as you go asynchronous, the user can start behaving concurrently. E.g., user presses a button that sends a request, and then the user presses another button for that device before the response comes in.

The proper way of handling this depends on exactly what those commands represent. I'd recommend first doing it in a simple way: disable the UI for that device while a command is in flight. Once you get that working, then you can look into more complex solutions like allowing queueing requests, complete with "job views", if that makes sense for your kind of devices/operations.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • thank for the clarification. So I should make my drivers and adapter methods async. My app will need to make many measurements at the same time (which different devices) , which can run for hours or in some cases days. At the same time, the application shpuld still be used to do other things show data, export data, start new measurements, etc. (The app is the main software of our Production & QS teams) If I understand it right, should not be better to move the whole work of a measurement to a background thread and use Progress for each measure result? – Rafa Gomez Jan 10 '21 at 16:21
  • 1
    I don't see a need for a background thread. Progress could be useful, though. – Stephen Cleary Jan 10 '21 at 16:32