I have some code that is using Task<T>
which defers returning a result from a serial read operation for a short time, like this:
void ReturnResponseAfterAShortDelay()
{
if (delayedResponseCancellationTokenSource != null)
delayedResponseCancellationTokenSource.Cancel(); // Cancel any pending operations and start a new one.
delayedResponseCancellationTokenSource = new CancellationTokenSource();
log.InfoFormat("Deferring response for {0} ms", Settings.Default.TimeoutMs);
Task.Delay(Properties.Settings.Default.TimeoutMs, delayedResponseCancellationTokenSource.Token)
.ContinueWith((continuation) => ReturnWhateverHasArrived(), TaskContinuationOptions.NotOnCanceled)
.Start();
}
The idea behind this code is to return the result when no new characters have arrived for a specified interval.
However, due to factors outside of my control, I must use .NET 3.5, which prevents me using Task<T>
, so I have to refactor this code somehow.
How can I achieve the same result, without using Task<T>
?
Although the specific code I showed happens to be a timed delay, my usage isn't limited to delaying things. There may be other cases where I want to start some 'long running' polling task immediately. A typical situation would be an I/O bound operation, for example something the periodically queries a device attached to the serial port and then raises an event when some condition is met.
Although the specific code I showed happens to be a timed delay, my usage isn't limited to delaying things. There may be other cases where I want to start some 'long running' polling task immediately. A typical situation would be an I/O bound operation, for example something the periodically queries a device attached to the serial port and then raises an event when some condition is met.
A notable and handy hack for coding such scenarios with C# 2.0 - 4.0 is to use self-driven IEnumerable
and yield
. It allows to implement an asynchronous state machine, similar to async/await
of C# 5.0. This way you keep the convenient linear code flow for your asynchronous logic. All C# language code control statements work (besides you can't do yield return
from inside try/catch
).
For example, a console app with a timer:
using System;
using System.Collections;
using System.Threading;
namespace ConsoleApplication_22516303
{
class Program
{
class AsyncLogic
{
public EventHandler Completed = delegate { };
IEnumerable WorkAsync(Action nextStep)
{
using (var timer = new System.Threading.Timer(_ => nextStep()))
{
timer.Change(0, 500);
var tick = 0;
while (tick < 10)
{
// resume upon next timer tick
yield return Type.Missing;
Console.WriteLine("Tick: " + tick++);
}
}
this.Completed(this, EventArgs.Empty);
}
public void Start()
{
IEnumerator enumerator = null;
Action nextStep = () => enumerator.MoveNext();
enumerator = WorkAsync(nextStep).GetEnumerator();
nextStep();
}
}
static void Main(string[] args)
{
var mre = new ManualResetEvent(false);
var asyncLogic = new AsyncLogic();
asyncLogic.Completed += (s, e) => mre.Set();
asyncLogic.Start();
mre.WaitOne();
Console.WriteLine("Completed, press Enter to exit");
Console.ReadLine();
}
}
}
Any event could be wrapped with a handler which would call nextStep
, similar to the above timer callback. The code would continue after the corresponding yield return
, upon the event.
There are quite a few implementations taking advantage of this approach, e.g., Jeffrey Richter's AsyncEnumerator
.