Continuous polling using Tasks
Asked Answered
J

2

6

This is something I've always used Threads / BackgroundWorker for, but am trying to migrate over to Task way of doing things.

Let's say I have a 3rd party SDK I use to read bytes from a USB port. That read call is blocking and times out after 100 ms if no bytes are read, returning null. It returns immediately if bytes are read, returning byte[] array of read bytes.

So I basically need to keep polling over and over, and take action on received bytes, by calling Parsing function. It's a WPF application, so the returned bytes should be able to be passed to a UI thread function.

What's the right approach to doing this? This is what I have so far, and it seems to work, but I want to make sure it's the right way of doing things using TPL:

private void _connectUsbButton_Click(object sender, RoutedEventArgs e)
{
   ListenForUsbMessagesAsync();
}

private async void ListenForUsbMessagesAsync()
{
    while (true)
    {
        byte[] readBytes = await ReadBytesAsync();
        Parse(readBytes);
    }
}

private Task<byte[]> ReadBytesAsync()
{
    Task<byte[]> readBytesTask = Task.Run(() =>
    {
        byte[] bytes;

        do
        {
            bytes = ReadBytes();
        } while (bytes == null);

        return bytes;
    });

    return readBytesTask;
}

private byte[] ReadBytes()
{
   byte[] readBytes = _usbSdk.ReadBytes(); //100ms timeout (returns null if no bytes read)
   return readBytes;
}
Junko answered 13/1, 2017 at 13:20 Comment(9)
Task<T> is for an eventual result, not one that happens periodically.Ucayali
@DanielA.White that's why I keep calling it in the while (true) loop. The eventual result being some bytes being read. The bytes are not read all the time, in fact most of the time there are no bytes being read.Junko
why didn't you like Thread for this kind of task? Tasks are designed to finish quickly, while your IO could take minutesPithead
@Pithead Tasks use Threads from the ThreadPool. So there is no much difference, except is a little bit easier to access stuff onto the UI ThreadUnclear
SInce your ReadBytesAsync might wait for long times (i.e more than a couple of seconds) it might be a good idea to run it on a dedicated thread, you can ask it from the tpl by passing a TaskCreationOptions.LongRunning in when creating your polling taskTrescott
@Pithead Tasks are abstracting threads, and should be used in all new projects. I've seen it stated that if you're using Thread objects in your greenfield projects, you are doing it wrong (by writing what is essentially legacy code).Junko
@Eternal21, stated where?Pithead
@MatteoMarciano-MSCP Tasks are an abstraction of asynchronous operations. One type of asynchronous operation is work being done in another thread. There are other types of asynchronous operations however that have nothing to do with using other threads. For example, waiting for IO to happen (like, say, what the OP is doing here) requires no additional threads at all.Dwelt
@Pithead In the following book: "Concurrency in C# Cookbook". Chapter 1 Tip: 'As soon as you type new Thread(), it's over; your project already has legacy code'. It also says: 'None of the multithreading recipes in this book use Thread or BackgroundWorker types; they have been replaced with superior alternatives.'Junko
U
6

Looks ok to me, just few suggestions here:

private async Task ListenForUsbMessagesAsync(CancellationToken token)
{
    while (true)
    {
        byte[] readBytes = await ReadBytesAsync();
        Parse(readBytes);
        token.ThrowIfCancellationRequested();
    }
}

Somewhere else, like in WPF Window .ctor store this

var tokenSource = new System.Threading.CancellationTokenSource();

Finally call your function like this

 private void _connectUsbButton_Click(object sender, RoutedEventArgs e)
 {
    ListenForUsbMessagesAsync(tokenSource.Token);
 }

This way you can cancel your task in any moment by calling

tokenSource.Cancel()

Alternatively, if you don't want to use Tasks, you can spawn a new Thread and pass in the Dispatcher object. In this way, the newly created Thread can fire stuff onto the UI Thread safely.

Unclear answered 13/1, 2017 at 13:33 Comment(0)
T
6

Since your polling task might run for a long time, you should think about running it in a dedicated thread.

you can achieve this by passing a TaskCreationOptions.LongRunning flag when creating the polling task.

like this:

Task<byte[]> readBytesTask = Task.Factory.StartNew(() =>
    {
        byte[] bytes;

        do
        {
            bytes = ReadBytes();
        } while (bytes == null);

        return bytes;
    }, TaskCreationOptions.LongRunning);
Trescott answered 13/1, 2017 at 13:52 Comment(3)
Tried adding your suggestion, but it ends up with a compiler error at the 'return bytes' line stating: CS0029 Cannot implicitly convert type 'byte[]' to 'System.Threading.Tasks.Task'Junko
fixed it, you need to use the Task.Factory to be able to pass a TaskCreationOptions (forget this overload is only available under the Factory)Trescott
For posterity, TaskCreationOptions.LongRunning does not guarantee that the task will run in a dedicated thread -- it is just a hint. In practice, however, on desktop and server systems, it is likely to. However, if what you want is a long-running poller/infinite loop service I'd suggest just making a thread for it and do your work there. If you are invoking awaitables, you can safely call .Result or .Wait() on the Task object from your own thread context without risking deadlocks.Khmer

© 2022 - 2024 — McMap. All rights reserved.