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.
CodePudding user response:
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);
}
}