Asynchronous Programming patterns

Asynchronous Programming Patterns

Asynchronous programming techniques or patterns are commonly used to handle long-running or potentially blocking operations. These patterns are designed to improve application performance, resource utilization, and scalability by allowing multiple operations to run concurrently and freeing up resources while waiting for an operation to complete.

Fire and Forget

  • A programming pattern in which a method or function is executed asynchronously without waiting for the completion of the task.
  • This pattern is typically used in scenarios where the result of the operation is not needed, and the primary goal is to avoid blocking the calling thread.
  • Once the asynchronous task is started, the application proceeds with other operations without waiting for the task to complete.
    internal static class Program
    {
        static void Main(string[] args)
        {
            Task.Run(async () =>
            {
                await LongRunningOperation();
            }).FireAndForget(ex =>
            {
    
                Console.WriteLine($"An error occurred: {ex.Message}");
            });
    
            Console.WriteLine("Operation started.");
            Console.ReadLine();
        }
    
        static async Task LongRunningOperation()
        {
            await Task.Delay(3000);
            throw new Exception();
        }
    
        public static void FireAndForget(this Task task, Action<Exception> errorHandler = null)
        {
            task.ContinueWith(t =>
            {
                if (t.IsFaulted && errorHandler != null)
                    errorHandler(t.Exception);
            }, TaskContinuationOptions.OnlyOnFaulted);
        }
    }

OnFailure

  • It is an extension method that can be used with Task objects in C#. It allows you to specify an action that will be executed if the task fails due to an exception.
  • ? OnFailure is particularly useful when you have a long-running task that is likely to fail at some point. ?
  • This method takes an action as a parameter, which is executed if the task fails.
  • This action is passed to the exception that caused the task to fail and can be used to log the error, notify the user, or take other appropriate action.
    internal static class Program
    {
        static async Task Main(string[] args)
        {
            string url = "https://example.com/data";
            await RetrieveAsync(url).OnFailure(ex => Console.WriteLine($"Failed to retrieve data: {ex.Message}"));
            Console.WriteLine("hello");
        }
    
        public static async Task OnFailure(this Task task, Action<Exception> onFailure)
        {
            try
            {
                await task.ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                onFailure(ex);
            }
        }
    
        private static async Task<string> RetrieveAsync(string url)
        {
            await Task.Delay(3000).ConfigureAwait(false);
            throw new Exception("Failed to retrieve data from server");
    
        }
    }

Timeout

  • Timeout is a mechanism to limit the amount of time a particular operation can take to complete.
  • This can be achieved using the Task.WhenAny method to wait for the first task to complete, and then checking if it was the expected task.
  • If not, the timeout has occurred, and the operation should be canceled.
    internal static class Program
    {
        static async Task Main(string[] args)
        {
            string url = "https://example.com/data";
            try
            {
                await RetrieveAsync(url).WithTimeout(TimeSpan.FromSeconds(5));
                Console.WriteLine("Data retrieved successfully");
            }
            catch (TimeoutException)
            {
                Console.WriteLine("Data retrieval timed out");
            }
        }
    
        public static async Task WithTimeout(this Task task, TimeSpan timeout)
        {
            var delayTask = Task.Delay(timeout);
            var completedTask = await Task.WhenAny(task, delayTask);
            if (completedTask == delayTask)
            {
                throw new TimeoutException();
            }
            await task.ConfigureAwait(false);
        }
    
        private static async Task<string> RetrieveAsync(string url)
        {
            await Task.Delay(6000).ConfigureAwait(false);
            return "Data";
        }
    }
    

Retry

  • This method takes the maximum number of retries and the delay between each retry as parameters.
  •  Retry can be useful when dealing with unreliable or slow external services, where retrying the operation can improve the success rate of the overall process.
  • It attempts to execute a Func<Task> delegate, which represents an asynchronous operation a certain number of times in case of failures.
    internal class Program
    {
        static async Task Main(string[] args)
        {
            string url = "https://example.com/data";
            int maxRetries = 3;
            TimeSpan delayBetweenRetries = TimeSpan.FromSeconds(1);
    
            try
            {
                var data = await Retry(async () => await FetchDataFromServerAsync(url), maxRetries, delayBetweenRetries);
                Console.WriteLine($"Fetched data: {data}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Failed to fetch data after {maxRetries} attempts: {ex.Message}");
            }
        }
    
    
        public static async Task<TResult?> Retry<TResult>(Func<Task<TResult>> taskFactory, int maxRetries, TimeSpan delay)
        {
            for (var i = 0; i < maxRetries; i++)
            {
                try
                {
                    return await taskFactory().ConfigureAwait(false);
                }
                catch
                {
                    if (i == maxRetries - 1)
                    {
                        throw;
                    }
                    await Task.Delay(delay).ConfigureAwait(false);
                }
            }
            return default;
        }
    
    
        private static async Task<string> FetchDataFromServerAsync(string url)
        {
            await Task.Delay(3000).ConfigureAwait(false);
            throw new NotImplementedException();
            //return "data from the server";
        }
    }

Fallback

  • This technique is used in software development to provide a secondary or alternative option in case the primary option fails or becomes unavailable.
  • The fallback can be useful when dealing with external services, where it is important to have a backup plan in case the primary service is not available. 
    internal static class Program
    {
        static async Task Main(string[] args)
        {
            string url = "https://example.com/data";
            var data = await RetrieveAsync(url).Fallback("fallback data");
            Console.WriteLine($"Fetched data: {data}");
        }
    
        public static async Task<TResult> Fallback<TResult>(this Task<TResult> task, TResult fallbackValue)
        {
            try
            {
                return await task.ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Failed to retrieve data: {ex.Message}");
                return fallbackValue;
            }
        }
    
        private static async Task<string> RetrieveAsync(string url)
        {
            await Task.Delay(2000).ConfigureAwait(false);
            throw new Exception("Failed to retrieve data from server");
            //return "Data from server";
        }
    }

You find the code in my git repo: https://github.com/saitejkuralla/AsynchronousProgramming