Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

What is Synchronous and Asynchronous Callback in C#?

5.00/5 (4 votes)
10 Aug 2023CPOL5 min read 19.5K  
Asynchronous and synchronous callback in C# and when to use each of them
This article helps you understand Synchronous and Asynchronous Callbacks in C# and their appropriate use cases, including examples and considerations for efficiently handling short-lived tasks, I/O operations, concurrency, responsiveness, and scalability.

Introduction

What is an asynchronous and synchronous callback in C#? When and in which scenario can sync and async methods be used?

Background

In C#, both Synchronous and Asynchronous callbacks are mechanisms used to handle the completion of a task or an event. Callbacks allow us to specify a function or delegate to be executed when a certain operation is completed. They are commonly used when dealing with tasks that might take a significant amount of time to complete, such as I/O operations or network requests.

The main difference between synchronous and asynchronous callbacks lies in how they handle the flow of execution and potentially block the calling thread.

1. Synchronous Callback

A synchronous callback is a function or delegate that is executed immediately and blocks the calling thread until it completes. In other words, when a synchronous callback is invoked, the program will wait for it to finish before moving on to the next task. This can lead to potential performance issues and blocking of the main thread, which could make the application less responsive.

Example of a synchronous callback in C#:

C#
public void SynchronousCallbackExample()
{
    Action<string> callback = message => Console.WriteLine(message);
    DoSomethingSynchronously(callback);
}

public void DoSomethingSynchronously(Action<string> callback)
{
    // Simulate a time-consuming operation
    for (int i = 0; i < 5; i++)
    {
        callback($"Iteration {i}");
        Thread.Sleep(1000); // Simulate work
    }
}

2. Asynchronous Callback

An asynchronous callback, on the other hand, is used to execute code without blocking the calling thread. Instead of waiting for the callback to complete, the calling thread can continue executing other tasks while the callback is running in the background. This approach helps maintain the responsiveness of the application and can lead to better utilization of system resources.

Example of an asynchronous callback in C#:

C#
public async Task AsynchronousCallbackExample()
{
    Func<string, Task> callbackAsync = async message =>
    {
        await Task.Delay(2000); // Simulate non-blocking work asynchronously
        Console.WriteLine(message);
    };
    await DoSomethingAsynchronously(callbackAsync);
}

public async Task DoSomethingAsynchronously(Func<string, Task> callbackAsync)
{
    var tasks = new Task[15];

    for (int i = 0; i < 15; i++)
    {
        tasks[i] = callbackAsync($"Iteration {i}");
        //await Task.Delay(1000);
    }

    await Task.WhenAll(tasks);
}

By using a Func<string, Task> delegate and an array of Task objects, we achieve concurrent execution of asynchronous operations without blocking the main thread. The await Task.WhenAll(tasks) line ensures that the program continues once all the asynchronous tasks have been completed.

Overall, asynchronous callbacks are preferred in scenarios where long-running operations are involved, as they help improve the responsiveness of the application and prevent blocking the main thread.

The purpose of using Func and a Task array in the example.

  1. Using Func for Asynchronous Callbacks

    • In asynchronous programming, we want to execute tasks concurrently without blocking the main thread.
    • The Func<string, Task> is a delegate that represents an asynchronous function that takes a string parameter and returns a Task. This delegate is used to define an asynchronous callback that can be executed concurrently.
  2. Defining the Asynchronous Callback

    • The callbackAsync function is defined using the Func<string, Task> delegate. It represents an asynchronous operation that simulates non-blocking work by delaying for a certain amount of time and then writing a message to the console.
  3. Using a Task Array to Store Asynchronous Operations

    • To perform multiple asynchronous operations concurrently, we create an array of Task objects to store these tasks.
  4. Looping and Adding Tasks to the Array

    • Inside the DoSomethingAsynchronously method, we iterate through a loop and create multiple tasks by invoking the callbackAsync function. Each task represents an independent asynchronous operation.
  5. Concurrent Execution Using Task.WhenAll

    • After adding all the tasks to the array, we use Task.WhenAll(tasks) to asynchronously wait for all the tasks to complete. This method returns a new task that completes when all the provided tasks have completed.

When and in Which Scenario, Sync and Async Methods Should be Used?

The choice between using synchronous (sync) and asynchronous (async) methods depends on the specific scenario and the requirements of the application. Here are some guidelines to help us decide when to use each approach.

Synchronous Methods

i. Simple and Quick Tasks: For operations that are short-lived, non-blocking, and don't involve significant waiting, synchronous methods can be appropriate. These methods are easier to read and understand.

ii. GUI or UI Thread: In user interface (UI) applications, certain operations must run on the main UI thread. Synchronous methods can be used for tasks that need to interact with the UI.

iii. Sequential Logic: When the code relies on sequential execution and we need to ensure that certain tasks are completed before moving on, synchronous methods might simplify our code.

Asynchronous Methods

i. I/O-Bound Operations: When performing I/O operations (such as reading/writing files, making network requests, or database queries), asynchronous methods are beneficial. They allow other tasks to continue while waiting for I/O to complete, leading to improved responsiveness.

ii. Concurrency and Parallelism: Asynchronous methods are essential for tasks that can run concurrently or in parallel, such as handling multiple requests or processing large amounts of data.

iii. Scalability: In server applications, asynchronous methods can help handle a larger number of incoming requests without creating a new thread for each request, which would be inefficient and resource-intensive.

iv. Long-Running Tasks: For operations that take a significant amount of time, such as complex calculations or background processing, asynchronous methods can prevent the application from becoming unresponsive.

v. Event-Based Programming: Asynchronous methods are often used in event-driven programming, where we need to respond to events without blocking the main thread.

vi. Multithreading: Asynchronous methods can simplify multithreading scenarios by allowing multiple tasks to run concurrently without directly managing threads.

In general, asynchronous methods are favored for scenarios involving potentially long-running or blocking operations, concurrency, responsiveness, and scalability. However, it's important to note that using async programming introduces some complexity, such as handling exceptions and synchronization, which should be carefully managed.

When choosing between synchronous and asynchronous methods, consider the nature of the task, the potential impact on responsiveness, and the overall architecture of our application. A balanced approach might involve a combination of both sync and async methods based on the specific needs of different parts of our codebase.

Points of Interest

One silly error that I was faced with, "The asynchronous action method returns a task which cannot be executed synchronously".

This error message typically occurs when we try to call an asynchronous method synchronously, which is not allowed in .NET. Asynchronous methods that return a Task are designed to be awaited asynchronously to avoid blocking the calling thread.

To fix this error, we should follow the asynchronous programming pattern and use the await keyword to asynchronously wait for the Task to complete. Here's an example of how to fix the error:

C#
using System;
using System.Threading.Tasks;
using System.Web.Mvc;

public class MyController : Controller
{
    public async Task<ActionResult> MyAction()
    {
        // Perform asynchronous operations here
        await DoAsyncWork();

        return View();
    }

    public async Task DoAsyncWork()
    {
        // Simulate a time-consuming operation asynchronously
        await Task.Delay(1000);
    }
}

In the example above, the MyAction method is marked as async and returns a Task<ActionResult>. Inside the method, we use the await keyword to asynchronously wait for the completion of the DoAsyncWork method, which is also marked as async.

By following this pattern, we ensure that our asynchronous methods are awaited asynchronously, preventing the blocking of the calling thread and avoiding the error message.

History

  • 10th August, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)