The .NET Framework has a rich asynchronous programming model (APM) that’s about as close to “fire and forget” as you’re likely to want. True “fire and forget” usually ends up being “fire and forget until it crashes,” so you want notification when an asynchronous task completes.
There are other asynchronous design patterns in .NET, including an event-based asynchronous pattern, the BackgroundWorker, and the Task Parallel Library, but the pattern I linked above has been around since the beginning and is still quite useful.
I call this the “callback design pattern.” The idea is that you tell the system, “perform this operation on a separate thread and call me back when it’s done.” In the example below, the MyThing
class has BeginTask
and EndTask
methods that operate as recommended by the APM.
void DoSomething(MyThing thing) { IAsyncResult ir = thing.BeginTask(TaskDoneCallback, thing); // Not doing anything with ir, yet. } // This method is called when the operation has completed. void TaskDoneCallback(IAsyncResult ir) { var thing = (MyThing)ir.AsyncState; var rslt = thing.EndTask(ir); // do something with the computed result }
The idea here is pretty simple. The call to BeginTask
starts the operation executing on a separate thread. When the operation is done, it calls TaskDoneCallback
(still executing on that other thread), which can then process the result as required.
The above works great. Let me add a new wrinkle to make it more interesting.
Imagine now that you want the asynchronous task to run continually so that it can generate data whenever it’s available. What you want is for the callback to start a new asynchronous task, like this:
void TaskDoneCallback(IAsyncResult ir) { var thing = (MyThing)ir.AsyncState; var rslt = thing.EndTask(ir); // Start a new async task thing.BeginTask(TaskDoneCallback, thing); // now do something with the computed result }
Here, the callback function gets the results of a task, starts a new asynchronous task, and then processes the results returned by the first task.
Again, that all works great. You can call BeginTask
once in your main program, and that task gets performed forever until you cancel it. Assuming, of course, that you’ve added the cancellation code.
Okay, so it works great most of the time. There’s an edge case in which this can lead to unbounded stack space usage.
You see, there’s the possibility that an asynchronous call could complete synchronously. That is, something in the runtime environment determines that there’s no need (or perhaps no ability) to spin up a new thread so that it can execute the task asynchronously. Instead, it will execute the task on the calling thread. When that happens, TaskDoneCallback
is executed on the calling thread. If this happens repeatedly, then the main thread begins looping and consuming stack space. It behaves as though you’d written this:
void BeginDoTask() { TaskDoneCallback(); } void TaskDoneCallback() { BeginDoTask(); }
That, as you well know, will blow the stack in short order.
The solution to this problem is kind of ugly:
void AsyncLoop() { for ( ; ; ) // Infinite loop! { IAsyncResult ir = thing.BeginTask(TaskDoneCallback, thing); // see if it completed synchronously if (!ir.CompletedSynchronously) break; this.InvokeEnd(ir); } } void TaskDoneCallback(IAsyncResult ir) { if (ir.CompletedSynchronously) return; this.InvokeEnd(ir); this.AsyncLoop(); // Start another async operation } void InvokeEnd(IAsyncResult ir) { var thing = (MyThing)ir.AsyncState; var rslt = thing.EndTask(ir); // now do something with the computed result }
If the method executes asynchronously, then things work exactly as I showed earlier. But if the method completes synchronously, the callback function executes immediately and the AsyncLoop
loop calls InvokeEnd
to process the data.
There is one other difference of note in this code: it processes the computed result before making the next asynchronous call. The result is if the “do something with computed result” takes any significant time, the program could lose data. This is a good argument for making your processing take as little time as possible. If it’s going to take more than a few milliseconds, you probably want to queue the item for processing by another thread.
In truth, I’ve never seen this infinite recursion problem happen, and off the top of my head I can’t imagine a real-world scenario in which it would be a problem. I can, however, contrive some cases. Although I can’t see myself writing code that would suffer from this problem (basically, writing a tight loop as a chain of asynchronous calls), I can imagine others doing it.
I’d put this one far down on the list of things that can go wrong with your asynchronous code. If you’ve written code that works as I described initially, then I wouldn’t worry about changing it to the new model unless all the other bugs in your program are fixed.