Overview of Asynchronous Execution in .NET
The .NET Framework provides mechanisms to execute methods asynchronously without blocking the calling thread. This is achieved by defining a delegate matching the signature of the target method. The Common Language Runtime (CLR) automatically generates BeginInvoke and EndInvoke methods for any given delegate instance.
BeginInvoke initiates the asynchronous operation and accepts parameters identical to the target method, plus two optional arguments:
- A callback delegate executed upon completion.
- A user-defined state object passed to the callback.
This method returns immediately, providing an IAsyncResult object used to track progress. Conversely, EndInvoke retrieves the return value. If called before completion, it blocks the current thread until the operation finishes.
Understanding IAsyncResult
The IAsyncResult interface is central to managing background tasks. It exposes four key properties:
Synchronous Execution Model
To illustrate the difference, consider a simulated background service representing a lengthy I/O operation, such as processing large datasets. We define a DataProcessor class:
public int Process(int timeout)
{
// Simulate intensive work
Thread.Sleep(timeout);
_progressPercent = 100;
return _progressPercent;
}
public int GetCurrentProgress() => _progressPercent;
}
</div>In a synchronous client scenario, invoking this method halts the execution flow. Consider a form button handling synchronous requests:
<div>```
private void btnSynchronous_Click(object sender, EventArgs e)
{
logBox.AppendText("Starting data processing...\r\n");
var processor = new DataProcessor();
// Main thread blocks here
int result = processor.Process(5000);
logBox.AppendText("Processing complete.\r\n");
logBox.AppendText($"Result: {result}\r\n");
// UI becomes unresponsive during the 5-second wait
}
Asynchronous Execution with Delegates
To prevent UI freezing, we utilize the asynchronous pattern. We wrap the DataProcessor logic within a class managing delegate invocation.
public AsyncOperationManager(DataProcessor service)
{
_service = service;
_execDelegate = new OperationDelegate(service.Process);
}
public IAsyncResult StartProcessing(int duration, AsyncCallback callback, object state)
{
return _execDelegate.BeginInvoke(duration, callback, state);
}
public int StopProcessing(IAsyncResult token)
{
if (token == null) throw new ArgumentNullException(nameof(token));
return _execDelegate.EndInvoke(token);
}
}
</div>The event handler now initiates the job and immediately continues to the next line of code:
<div>```
private void btnAsynchronous_Click(object sender, EventArgs e)
{
logBox.AppendText("Job queued...\r\n");
var worker = new AsyncOperationManager(new DataProcessor());
// Non-blocking call
IAsyncResult context = worker.StartProcessing(5000, null, null);
logBox.AppendText("Doing other tasks while waiting...\r\n");
// Blocks only when retrieving the result
int finalValue = worker.StopProcessing(context);
logBox.AppendText($"Completion confirmed. Value: {finalValue}\r\n");
}
Polling for Completion
Relying solely on EndInvoke implies blocking at the moment of retrieval. Alternatively, we can poll the IAsyncResult to check status without constant blocking. Reducing the simulation time helps demonstrate this loop:
int iteration = 0;
while (!ctx.IsCompleted)
{
iteration++;
Application.DoEvents(); // Allow UI updates
}
logBox.AppendText($"Polling finished after {iteration} checks.\r\n");
int value = manager.StopProcessing(ctx);
}
</div>### Using WaitHandles
Pollling consumes CPU cycles. A more efficient approach involves waiting on the `AsyncWaitHandle`. This blocks the thread untill a signal is received or a timeout occurs.
<div>```
private void btnWaitHandle_Click(object sender, EventArgs e)
{
logBox.AppendText("Waiting via Handle...\r\n");
var mgr = new AsyncOperationManager(new DataProcessor());
IAsyncResult ctx = mgr.StartProcessing(5000, null, null);
// Wait up to 3 seconds for the signal
bool signaled = ctx.AsyncWaitHandle.WaitOne(3000);
if (signaled)
{
logBox.AppendText("Operation completed within timeout.\r\n");
}
else
{
logBox.AppendText("Timeout exceeded before completion.\r\n");
// Logic to handle timeout could go here
}
ctx.AsyncWaitHandle.Close();
}
The most robust pattern passes a callback method during initiation. When the operation finishes, the runtime invokes this delegate automatically. To update the UI safely from the background thread, we use Control.Invoke.
We modify the management class to accept an action for completion notification:
public NotificationManager(DataProcessor target, Action<int> uiCallback)
{
_target = target;
_uiCallback = uiCallback;
_workInvoker = new WorkDelegate(target.Process);
}
public void LaunchWork(int duration)
{
_workInvoker.BeginInvoke(duration, OnWorkFinished, null);
}
private void OnWorkFinished(IAsyncResult ar)
{
try
{
int result = _workInvoker.EndInvoke(ar);
// Marshal back to UI thread
InvokeOnMain(_uiCallback, result);
}
catch (Exception ex)
{
// Handle errors appropriately
}
}
private void InvokeOnMain(Action<int> callback, int value)
{
// Implementation depends on Context
logBox.Invoke(new MethodInvoker(() =>
{
logBox.AppendText($"Callback fired: {value}\r\n");
}));
}
}
</div>The main interaction simply fires the task and steps away:
<div>```
private void btnCallback_Click(object sender, EventArgs e)
{
logBox.AppendText("Fire and forget mode...\r\n");
var notifier = new NotificationManager(new DataProcessor(), UpdateLog);
notifier.LaunchWork(3000);
logBox.AppendText("Main thread continues immediately.\r\n");
}