Brian O
Brian O

Reputation: 41

How can I instantly return values from an async loop?

I've got an async discover function which discovers devices in the local network which I call on a button click. I'm sending a broadcast message to all devices and listen for responses for 5 seconds using a CancellationTokenSource. After the token has expired I'm returning an IEnumerable of parsed responses to my WPF model.

I'd like return incoming responses directly (and stop listening after 5 seconds) so that I can show discovered devices instantly in the UI instead of showing them all after 5 seconds.

This is my code:

public async Task<IEnumerable<IDevice>> Discover()
{
    var client = new MyClient();
    var responseData = await GetResponseData(client);
    return this.ParseResponseData(responseData);
}

private IEnumerable<IDevice> ParseResponseData(List<DeviceResponseData> responseData)
{
    foreach (var data in responseData)
    {
        yield return DeviceFactory.Create(data);
    }
}

private static async Task<List<DeviceResponseData>> GetResponseData(MyClient client,
    int timeout = 5000)
{
    var cancellationToken = new CancellationTokenSource(timeout);
    var data = new List<DeviceResponseData>();

    // ... prepare message and send it
    await client.SendAsync(message, new CancellationToken());

    try
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            // Wait indefinitely until any message is received.
            var response = await client.ReceiveAsync(cancellationToken.Token);

            data.Add(new DeviceResponseData(/* ... */ response));
        }
    }
    catch (TaskCanceledException e)
    {

    }

    return data;
}

Upvotes: 3

Views: 284

Answers (2)

Theodor Zoulias
Theodor Zoulias

Reputation: 43495

If you have upgraded to C# 8, you have the option of returning an IAsyncEnumerable. This mechanism can propagate a stream of DeviceResponseData objects, that can be consumed as soon as they are produced. Here is the producer method, that is implemented as an iterator (contains yield statements).

private static async IAsyncEnumerable<DeviceResponseData> GetResponseDataStream(
    MyClient client, int timeout = 5000)
{
    using CancellationTokenSource cts = new CancellationTokenSource(timeout);

    // Prepare message...
    try
    {
        await client.SendAsync(message, cts.Token).ConfigureAwait(false);
    }
    catch (OperationCanceledException) { throw new TimeoutException(); }

    while (!cts.IsCancellationRequested)
    {
        Response response;
        try
        {
            // Wait until any response is received.
            response = await client.ReceiveAsync(cts.Token).ConfigureAwait(false);
        }
        catch (OperationCanceledException) { yield break; }

        yield return new DeviceResponseData(/* ... */ response);
    }
}

And here is how it can be consumed:

await foreach (DeviceResponseData data in GetResponseDataStream(client))
{
    // Do something with data
}

The best part is that you don't need to add synchronization code (Dispatcher.Invoke etc) every time you receive a DeviceResponseData object. The async/await machinery restores the captured synchronization context automatically for you (unless you tell it to do otherwise, by using the ConfigureAwait method).

Important: Make sure that you dispose the CancellationTokenSources you create.

Upvotes: 0

TheGeneral
TheGeneral

Reputation: 81493

If you want to show results as they come in, there are many ways of achieving this, like decoupled messages, events, etc.

However, you could just use a simple Action

private static async Task<List<DeviceResponseData>> GetResponseData(MyClient client, Action<DeviceResponseData> update, int timeout = 5000)
{
   var cancellationToken = new CancellationTokenSource(timeout);
   ...
   while (!cancellationToken.IsCancellationRequested)
   {
      // Wait indefinitely until any message is received.
      var response = await client.ReceiveAsync(cancellationToken.Token);

      var result = new DeviceResponseData( /* ... */ response);

      data.Add(result);
      update(result);

   }
   ...
}

usage

var allResults =  await GetResponseData(client,data => UdpateUI(data), timeout);

Note : because this the Async Await Pattern, you wont have to marshal the result form the Action back to the UI Context, if that's where this was called from.

Upvotes: 1

Related Questions