Synchronous vs Asynchronous Delegates in Legacy C

In legacy C# (.NET Framework versions prior to modern .NET Core), developers handled method execution through delegates either synchronously using direct invocation or asynchronously using the Asynchronous Programming Model (APM). The fundamental difference between the two approaches lies in thread management, responsiveness, and execution flow: synchronous invocation runs directly on the calling thread and blocks execution until the operation completes, whereas the legacy asynchronous pattern (BeginInvoke and EndInvoke) dispatches work to a ThreadPool thread to allow non-blocking, parallel operations.

Synchronous Delegate Invocation

Synchronous delegate execution occurs when a delegate is invoked directly, either using standard method call syntax or explicitly via the .Invoke() method:

Func<int, int, int> add = (a, b) => a + b;
int result = add(5, 10); // Or add.Invoke(5, 10);

When invoked synchronously:

  • Thread Execution: The delegate executes on the exact thread that called it.
  • Blocking Behavior: The calling thread suspends execution until the delegate finishes its work. If the operation is I/O-heavy or computationally demanding, it halts UI responsiveness or stalls server request processing.
  • Exception Propagation: Any exceptions thrown within the target method propagate directly up the calling thread's call stack, making them catchable with a standard try-catch block enclosing the invocation.

Legacy Asynchronous Pattern (BeginInvoke and EndInvoke)

Before the introduction of the Task-based Asynchronous Pattern (TAP) and the async/await keywords in C# 5.0, delegates automatically provided built-in asynchronous methods generated by the compiler: BeginInvoke and EndInvoke.

Func<int, int, int> add = (a, b) => a + b;

// Initiates execution on a ThreadPool thread
IAsyncResult asyncResult = add.BeginInvoke(5, 10, null, null);

// Do other work on the calling thread...

// Retrieves the return value and blocks if work is not yet finished
int result = add.EndInvoke(asyncResult);

When invoked asynchronously:

  • Thread Execution: BeginInvoke dispatches the method call to a worker thread managed by the CLR ThreadPool.
  • Non-Blocking Initiation: BeginInvoke returns immediately, providing an IAsyncResult token that tracks execution status. This leaves the calling thread free to continue processing other tasks.
  • Completion and Retrieval: The caller must call EndInvoke to retrieve return values, capture out/ref parameters, and clean up internal resources. If the worker thread has not finished when EndInvoke is called, the calling thread blocks at that point until completion.
  • Callback Support: BeginInvoke allowed passing an AsyncCallback delegate, which executed automatically on the worker thread once the task was complete, eliminating the need to poll or manually wait for the result.
  • Exception Handling: Exceptions thrown inside the delegate are not surfaced at the time of BeginInvoke. Instead, they are captured and re-thrown when the caller calls EndInvoke.

Key Technical Differences

Feature Synchronous Invocation (Invoke) Legacy Asynchronous (BeginInvoke/EndInvoke)
Execution Context Executes on the caller's thread Executes on a ThreadPool background thread
Caller Blocking Fully blocks the caller until completion Returns immediately; blocks only when EndInvoke is called
Resource Overhead Minimal; standard method-call overhead Higher; requires thread allocation, context switching, and state tracking via IAsyncResult
Exception Handling Caught immediately at the point of call Stored internally and re-thrown only when calling EndInvoke
Completion Notification Implicit upon return of the method Handled via polling, waiting on a WaitHandle, or an AsyncCallback

Obsolescence in Modern .NET

In modern .NET (.NET Core, .NET 5, and later), delegate-based BeginInvoke and EndInvoke are unsupported and throw a PlatformNotSupportedException. The runtime replaced this pattern with Task-based alternatives (Task.Run or native asynchronous I/O methods), which offer better performance, integration with cancellation tokens, and streamlined control flow through async and await.