Async-Lock Mechanism on Asynchronous Programming

Bora Kaşmer
The Startup
Published in
10 min readMay 8, 2020

Today, we will talk about how we can block the asynchronous working process in some special cases, which is a problem in lots of programming languages, and why we may need it.

“Do you have the patience to wait
Till your mud settles and the water is clear?
Can you remain unmoving
Till the right action arises by itself?”
Lao tzu

What is Asynchronous Programming?

Before talking about the “asynchronous” programming model, let’s talk about the “synchronous” programming model.

Synchronous:

Everythings happen at one a time. The process runs only as a result of some other process being completed or handed off. When you call a function, which performs a long-running action, it returns results only when the operation has finished. And this stops your program until it ends.

Asynchronous:

An asynchronous model allows multiple things to happen at the same time. The process operates independently of other processes. When you start an action, your program continues to run at the backend. And the most important thing, it never stops your program. When the operation finishes, the program is informed and gets access to the operation result (for example, like getting a customer list from dB).

Image Source

We will use .Net Core for the Backend Webservice. But remember, “Asynchronous” is an independent technology. The same rules apply to other languages. Let’s create the AsyncLock “.Net Core 3.1.200” WebApi project.

In this application, we will show some news data on a portal. Actually, this is a big News Portal. So firstly, we create a “News” model as below.

Models/News.cs:

using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
public class News
{
public int ID { get; set; }
public string Title { get; set; }
public string UrlTitle { get; set; }
public string Detail { get; set; }
public DateTime CreatedDate { get; set; }
public string Image { get; set; }
}

For not to distribute our focus, I will not get data from any database. So let’s create our static Lorem Ipsum News data on the News Constructor.

NewsController.cs-Part1:

For the performance, I used Redis Cache in this real-life scenario. If twenty thousand clients come to this News Portal simultaneously and receive data directly from the DB, we probably see that the DB does not respond to requests any more :) So we will use Redis as an in-Memory DB. All clients will get news data from the Redis, not from the DB. And we will refresh the Redis at regular intervals.

Setup Redis For Macbook:

  1. Firstly install HomeBrew for installing Redis.
  2. brew install redis”: Install Redis.
  3. “redis-server” :Run Redis.
  4. “redis-cli”,”ping” : Test Redis. You have to get the “pong” result if everything works fine.

Download below CLI Packages for using Redis and Newtonsoft on .Net Core:

.dotnet add package Microsoft.Extensions.Caching.Redis

.dotnet add package Microsoft.AspNetCore.Mvc.NewtonsoftJson

Now we have to make some config on Startup.cs.

Startup.cs: This file is used to set some configuration on .Net Core.
.As you see, we added NewtonsoftJson() library for using serialization. And we set up local Redis Connection settings. “127.0.0.1:6379” is the default Redis connection’s Ip.

Startup.cs:

public void ConfigureServices(IServiceCollection services){    services.AddControllersWithViews()
.AddNewtonsoftJson()
.AddJsonOptions(opts =>opts.JsonSerializerOptions
.PropertyNamingPolicy = null);
services.AddDistributedRedisCache(option =>{
option.Configuration = "127.0.01:6379";
});
}

Now Let’s write News WebApi Service With Redis :

1-) “_distributedCache = distributedCache;” : This is the Redis CacheObject, which use in application with dependency injection.

2-) “cacheKey” : Key of the NewsModel in Redis. In this application, it is “AsyncNewsData.”
“cacheTime” : Expire time in seconds at Redis. In this application, it is 30 seconds.

3-) “AddRedisCache()”: With this method, we will try to get all News data from the Redis asynchronously. If it is null, we will get it in real life from DB, but for this application, we will get it from a static List of News model objects. And we will fill the Redis with “AsyncNewsData” for thirty seconds asynchronously.

Tip: We call the return data as a “client1”. Because in this scenario, we have only one client. We responded only for this client.😉

NewsController.cs-Part2:

private readonly IDistributedCache _distributedCache;public NewsController(IDistributedCache distributedCache)
{
//.... We filled the static List<news> model was here...
_distributedCache = distributedCache; //Redis Cache
}
[HttpGet]
public async Task<List<News>> News()
{
string cacheKey= "AsyncNewsData";
int cacheTime = 30;
var client1 = await AddRedisCache(model, 30, "AsyncNewsData", "Client 1");
return client1;
}

NewsController.cs- Part3/AddRedisCache(): When a client gets a request from news() service, we check the Redis “asynchronously” for the “List<news>” model. If it is null, we get data from SQL DB (static List<News> model) and fill the Redis again “asynchronously.” In the end, we return the “Task<List<News>>” model to the client.

Photo by Lia Trevarthen on Unsplash

When Redis Could be null ?

1.When the first client comes to the server, Redis is empty.
2.If the Redis was filled at least 30 seconds ago, “AsyncNewsData” is Expired in Redis.

AddRedisCache():

Time passes, things change, but memories will always stay where they are, in the heart.
― Surbhi Jain

This is the result of the News WebApi screen “http://localhost:5000/news”. For one client, we got all the news data asynchronously, as you see below the picture.

Everything seems fine in this simple scenario. But I’m afraid real life is not that simple.

Scenario 1

Imagine you have a big News portal, and Twenty thousand people come to your portal, “ concurrently.”

Now we will test for three customers, who came to the portal at the same time. “Client1, Client2, and Client3” are these test users. For a test scenario, all users get news data asynchronously.

1-) Every “client” is a “Task.” All Tasks work parallel and async.
2-) ”allTasks” is a List of tasks.
3-) ”allTasks.AddRange()” : We added all “client” Task to this “allTasks” list.
4-) “await Task.WhenAll(allTasks)”: We wait for all tasks to be completed before returning the result to the clients.

[HttpGet]
public async Task<List<News>> News()
{
string cacheKey = "AsyncNewsData";
int cacheTime = 30;
var allTasks = new List<Task<List<News>>>(); var client1 = AddRedisCache(model, cacheTime, cacheKey, "Client 1");
var client2 = AddRedisCache(model, cacheTime, cacheKey, "Client 2");
var client3 = AddRedisCache(model, cacheTime, cacheKey, "Client 3");
allTasks.AddRange(new List<Task<List<News>>>() { client1, client2, client3 }); await Task.WhenAll(allTasks);
return allTasks.First().Result;
}

Why we create three clients? Because We want to simulate three clients who came from different machines and different browsers at the same time.

  1. In the “A” section, we tried to get News Data asynchronously from the Redis.
  2. After Redis Cache was Expired, as you see above the picture, all the clients passed the “B” condition because all the clients tried to get data at the same time asynchronously.
  3. Section “C” is the worst part of this code. All the clients try to get the NewsData from the DB. Imagine, if twenty thousand clients came concurrently and if Redis expired, all the clients try to get data from DB. However, for example, SQL or Oracle DB can no longer resist this overload and finally crashes.
  4. In section “D,” I wrote the client name to the console. Because I wanted to detect which client passed the “Redis null condition.” And again, I filled the Redis with the last news data for thirty seconds.
  5. In the last section, “E” returns News List data with the AddRedisCache() method.
This is the result screen of the console.

As you see above the picture, all the clients passed the null condition and wrote the client’s name on the console.

“Don’t face all the problems at once. Divide and solve them one by one. Small bites are always swallowed more easily.”
―Bora Kaşmer

Photo by iMattSmart on Unsplash

Solution Double Check Lock Object:

We will use “SemaphoreSlim” classes for the lock async methods. Actually, what is lock mean?

SemaphoreSlim is a hidden treasure for programmers. This is a fine line between juniar and experienced.

By I, Cburnett, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=2233464

All the Tasks have ‘a Thread Pool. What about forcing the Tasks to run only one active thread at a time.

Solution: This is meant that only one client actually thread can work at the same time. We call it “Lock Object” for the async methods.

AsycLock Class: This is lock object for async process. SemaphoreSlim class help us to working only one client at a time. SemaphoreSlim object is used to control the access to a resource like calling other API or limiting the I/O operations concurrently to avoid unnecessary network/hardware issues.

.”new SemaphoreSlim(1, 1)”=>Only one thread has access to the resource at a time.
.”m_toRelease.m_semaphore.Release()” =>Increments the semaphore counter. It is one for this application.

Now Let’s Solve The Problem:

We will change the code like this. But please do not hurry, I didn’t write the code on purpose yet.

Scenario 2

As you see the above picture, we added “ m_lock.LockAsync” Lock Object. It allows only one client to work at a time. 2. and 3. clients are waiting for the “LockAsync” object to unlocked.

Tip: All clients work asynchronously. So the first client who passes the lock object always changes. This means there is no queue in the Thread Pool. This time Client 1 moved first, but next time, Client 2 or Client 3 could be.

Is this enough? Unfortunately not. Let’s debug the code!

Once all clients have passed the Redis null condition (CHECK 1), they will go through the lock object (LOCK) one by one. Although this situation is slightly different from the previous one, it is still terrible. Let me explain:

In Scenario 1, all the clients connected to the DB and tried to fill Redis at the same time. This is catastrophic :) Server at the end will be crash.

In Scenario 2, all the clients connected to the DB and tried to fill Redis one by one this time. It will take so long:) But the server keeps going to work. Because Lock Object was only paused the other threads. We need something else.

We need the Double Check Lock Object. So far, we had one “Check Condition and one “Lock Object.” So we need one more “Check Condition” state for the solution.

Scenario 3:

As you see the above the picture, we added second check (CHECK 2) condition. But why? Let’s Debug the code.

  1. At the (CHECK 1) condition, we got the NewsData from the Redis and checked is it null or not.
  2. If the Redis is null, we put the Lock Object and forced it to a thread pool to run only one active thread at a time. While “Client 1”, getting data from DB and filling the Redis, other clients waited at the out of the lock object.
  3. After the process is finished, “Client 1, Client 2” or one of the other clients go through the Lock Object Block(LOCK).
  4. “Client 2” receives the data from the Redis a second time and checks if it is empty or not. Of course, after Client 1, Redis is full of NewsData. Therefore, when Redis (CHECK 2) checked by the “Client 2” second time, it seems that it is not empty as expected because the first customer has already taken the data from the DB and filled the Redis. “Client 2” does not enter into the CHECK 2 condition and skips. Finally, NewsData is taken from Redis and sent back to “Client 2”. All the story is the same for “Client 3” too.

This is the result Console Screen: “Only “Client 3” wrote the client’s name on the console.”
When the Redis Expired, only one client, “Client 3”, received News Data from the DB and filled into the Redis. Other Clients waited for Client 3 to end the transaction and collected data from the Redis, not from the DB.

Tip: This time first coming client was “Client 3”.

Conclusion:

With this article, we learned how to handle asynchronous methods when the Redis is expired, if Twenty Thousand customers come to our News Portal concurrently. When Redis is not expired, all three clients work at the same time. We call that parallel programming. But when Redis is expired, all the clients wait for each other. And they work synchronously.

THE END & GOOD BYE

“If you have read so far, first of all, thank you for your patience and support. I welcome all of you to my blog for more!”

Source Code: https://github.com/borakasmer/AsyncLockObject
Sources
: en.wikipedia.org, stackoverflow.com, c-sharpcorner.com, stacoverflow.com

--

--

Bora Kaşmer
The Startup

I have been coding since 1993. I am computer and civil engineer. Microsoft MVP. Software Architect(Cyber Security). https://www.linkedin.com/in/borakasmer/