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
.