0

I have the same requirement as the poster in this question. I want to wait for a function to return but also set a timeout value.

I want to use the part :

int timeout = 1000;
var task = SomeOperationAsync();
if (await Task.WhenAny(task, Task.Delay(timeout)) == task) {
    // task completed within timeout
} else { 
    // timeout logic
}

but I have no idea how to make my function async.

When sending SMS via a modem I initiate the sending sequence and then waits for the modem to reply that it is ready for the message part. All data that the modem sends to my software is categorized and then added to the appropriate Channel.

private async void WaitForReadyForSMSData()
{
    while (true)
    {
        ModemData modemData;
        while (!_sendSMSChannel.Reader.TryRead(out modemData))
        {
            Thread.Sleep(ModemTimings.ChannelReadWait);
        }
        if (modemData.ModemMessageClasses.Exists(o => o == ModemDataClass.ReadyForSMSData))
        {
            break;
        }
    }
}

This is the part I want to make an async function out of but I am clueless.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
jeppe
  • 59
  • 10
  • 1
    What are `_sendSMSChannel` and `_sendSMSChannel.Reader` ? You can replace `Thread.Sleep` with `Task.Delay` to avoid blocking. Change the return type to `async Task` instead of `async void` so you can await that method – Panagiotis Kanavos Dec 16 '22 at 09:16
  • And [this answer](https://stackoverflow.com/a/68998339/982149) doesn't work for you? – Fildor Dec 16 '22 at 09:16
  • That is one of the channels I use. https://learn.microsoft.com/en-us/dotnet/core/extensions/channels – jeppe Dec 16 '22 at 09:18
  • 1
    In that case why don't you use `ReadAsync` ? – Panagiotis Kanavos Dec 16 '22 at 09:18
  • I have looked at using ReadAsync but I don't know how to use it. I haven't worked with async before. – jeppe Dec 16 '22 at 09:21
  • 1
    Then simply try it first. Haven't done something not yet is the norm when programming. When you still stumble over using ReadAsync then ask again with the code you tried. – Ralf Dec 16 '22 at 09:25
  • As a side note the upcoming .NET 8 will produce a warning when a `Task.Delay` is used as an argument to `Task.WhenAny`, and that `Task.Delay` doesn't take a cancellation token: [Detect non-cancelable Task.Delay passed to Task.WhenAny](https://github.com/dotnet/runtime/issues/33805). – Theodor Zoulias Dec 16 '22 at 10:17

1 Answers1

3

Since Reader is a ChannelReader, it's better to use ReadAsync or ReadAllAsync instead of polling. The entire method can be replaced with :

var modemData =await _sendSMSChannel.Reader.ReadAsync();

ReadAsync can accept a CancellationToken to implement a timeout, although that may not be needed. Channels are typically used to create either work queues or pipelines. They're meant to be long lived.

var cts=new CancellationTokenSource(TimeSpan.FromMinutes(1));
var modemData =await _sendSMSChannel.Reader.ReadAsync(cts.Token);

This will through a timeout exception if no message is received for 1 minute.

In an SMS sending service though, there's no real need for such a timeout, unless one intends to shut down the service after a while. In this case it's better to call ChannelWriter.Complete() on the writer instead of nuking the reader.

Using async foreach

For normal operations, an asynchronous foreach can be used to read messages from the channel and send them, until the service shuts down through the writer. ReadAllAsync returns an IAsyncEnumerable that can be used for asynchronous looping

await foreach(var modemData in _sendSMSChannel.Reader.ReadAllAsync(cts.Token))
{
    if (modemData.ModemMessageClasses.Exists(o => o == ModemDataClass.ReadyForSMSData))
    {
        await _smsSender.SendAsync(modemData);
    }
}

When the time comes to shut down the channel, calling ChannelWriter.Complete or TryComplete will cause the async loop to exit gracefully

public async Task StopAsync()
{
    _sendSMSChannel.Complete();
}

Assuming you use a BackgroundService, the loop could go in ExecuteAsync and Complete in StopAsync

public override async ExecuteAsync(CancellationToken token)
{
    await foreach(var modemData in _sendSMSChannel.Reader.ReadAllAsync(cts.Token))
    {
        if (modemData.ModemMessageClasses.Contains(ModemDataClass.ReadyForSMSData))
        {
            await _smsSender.SendAsync(modemData);
        }
    }
}

public override async StopAsync(CancellationToken token)
{
    _sendSMSChannel.Complete();
    base.StopAsync(token);
}

IAsyncEnumerable and LINQ

System.Linq.Async provides LINQ operators on top of IAsyncEnumerable like Where. This can be used to filter messages for example :

public override async ExecuteAsync(CancellationToken token)
{
    var messageStream=_sendSMSChannel.Reader.ReadAllAsync(cts.Token)
                         .Where(msg=>msg.ModemMessageClasses.Contains(ModemDataClass.ReadyForSMSData));

    await foreach(var modemData in messageStream)
    {
        await _smsSender.SendAsync(modemData);
    }
}

It's possible to extract the filtering clause to a separate method :

public static IAsyncEnumerable<ModemData> this ReadyToSend(IAsyncEnumerable<ModemData> stream)
{
    return stream.Where(msg=>msg.ModemMessageClasses
                                .Contains(ModemDataClass.ReadyForSMSData));
}

...

public override async ExecuteAsync(CancellationToken token)
{
    var messageStream=_sendSMSChannel.Reader.ReadAllAsync(cts.Token)
                         .ReadyToSend();

    await foreach(var modemData in messageStream)
    {
        await _smsSender.SendAsync(modemData);
    }
}

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • You can add a CancellationToken for automatic timeout. => https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource.-ctor?view=net-7.0#system-threading-cancellationtokensource-ctor(system-timespan) – Fildor Dec 16 '22 at 09:23
  • The problem is this, the read modemData may or may not be what I am after. If not I need to continue reading for subsequent modemData. But I need to timeout at some point in time. Ah OK. I have to check that CancellationToken. – jeppe Dec 16 '22 at 09:26
  • @jeppe I added an example using `await foreach`. When and why do you want to time out though? The loop itself doesn't need to timeout. It's just waiting for some other thread to post messages to the channel. If you want to shut down the channel, call `Complete` on the writer – Panagiotis Kanavos Dec 16 '22 at 09:29
  • The channel name is misleading, it is just used when sending SMS and in this instance to know when the modem is ready for the SMS message data (PDU). Thanks for your input, it solved my problem. Haven't used channels before either. All new to me. – jeppe Dec 16 '22 at 09:44
  • I want to timeout because in case is something wrong with the communication with the modem. I am testing with two different models and sometimes there is comms problems, usually software bug/handling on my part or modem being slow in responding. – jeppe Dec 16 '22 at 09:48
  • Filtering also seems to be what I need. – jeppe Dec 16 '22 at 09:53
  • You don't have to use a Channel for one-off events. That's what `TaskCompletionSource` is for. If you need to process a stream of messages that includes a `ready to send`, there are ways to do this using Rx.NET and maybe System.Linq.Async as well. – Panagiotis Kanavos Dec 16 '22 at 09:53
  • Excellent, then I can use a single channel. I have to check that out. – jeppe Dec 16 '22 at 09:55
  • *"Channels [...] are meant to be long lived."* -- Citation? *"This will throw a timeout exception if no message is received for 1 minute."* -- Most likely an `OperationCanceledException`. *"ReadAsync(cts.Token)"* -- Beware of the [memory leak](https://stackoverflow.com/questions/67573683/channels-with-cancellationtokensource-with-timeout-memory-leak-after-dispose) issue. – Theodor Zoulias Dec 16 '22 at 09:55
  • My channels lives as long as the application runs. – jeppe Dec 16 '22 at 09:57