Five secrets of .NET async await

Zack Yang
6 min readApr 1, 2021

Async /await async operations, the amazing syntactic sugar of C#, make asynchronous programming beautiful and easy to implement. Even JavaScript borrows the async/await syntax to make callback-infested JavaScript code beautiful.

Secret One: Controlling the number of tasks executed in parallel

During software development, there are times when many tasks need to be executed asynchronously, but it is often necessary to limit the number of tasks that can be executed in parallel to avoid the performance degradation caused by too many asynchronous tasks being executed simultaneously. For example, when web crawlers fetch content from the web in parallel, the maximum number of execution threads should be limited according to the situation.

In the days before async/await, mechanisms such as semaphores were used to communicate between threads to coordinate the execution of threads, requiring the developer to know the technical details of multithreading. With async/await, everything can be done easily.

For example, the following code reads the words one by one from the file “words.txt”, which consists of words by line. Then it calls a remote API to get the details of the words, such as “phonetic symbols, Chinese meanings, example sentences”. To speed up processing, asynchronous programming is needed to enable simultaneous downloading of multiple tasks, but to limit the number of simultaneous tasks (let’s say five).The code is as follows:

class Program

{

static async Task Main(string[] args)

{

ServiceCollection services = new ServiceCollection();

services.AddHttpClient();

services.AddScoped<WordProcessor>();

using (var sp = services.BuildServiceProvider())

{

var wp = sp.GetRequiredService<WordProcessor>();

string[] words = await File.ReadAllLinesAsync(“d:/temp/words.txt”);

List<Task> tasks = new List<Task>();

foreach(var word in words)

{

tasks.Add(wp.ProcessAsync(word));

if(tasks.Count==5)

{

//wait when five tasks are ready

await Task.WhenAll(tasks);

tasks.Clear();

}

}

//wait the remnant which are less than five.

await Task.WhenAll(tasks);

}

Console.WriteLine(“done!”);

}

}

class WordProcessor

{

private IHttpClientFactory httpClientFactory;

public WordProcessor(IHttpClientFactory httpClientFactory)

{

this.httpClientFactory = httpClientFactory;

}

public async Task ProcessAsync(string word)

{

Console.WriteLine(word);

var httpClient = this.httpClientFactory.CreateClient();

string json = await httpClient.GetStringAsync(“http://dict.e.opac.vip/dict.php?sw=" + Uri.EscapeDataString(word));

await File.WriteAllTextAsync(“d:/temp/words/” + word + “.txt”, json);

}

}

The core code is the following:

List<Task> tasks = new List<Task>();

foreach(var word in words)

{

tasks.Add(wp.ProcessAsync (word));

if(tasks.Count==5)

{

//wait when five tasks are ready

await Task.WhenAll(tasks);

tasks.Clear();

}

}

The returned Task object is not modified with “await”. Instead, it stores the returned Task object in the list. Since we do not wait with “await”, we add the next Task to the list without waiting for the completion of one Task. When the list is full of five tasks, we call “await Task.whenAll (tasks);” to wait for these five tasks to complete before processing the next group. await Task.WhenAll(tasks) outside the loop is used to handle the last set of fewer than five tasks.

Secret Two: How to perform DI injection in BackgroundService

With Dependency Injection (DI), the injected objects have life cycles. For example, when using services.AddDbContext<TestDbContext>() to injected the DbContext object of EF Core, the lifetime of the TestDbContext is Scope. TestDbContext can be injected directly into a normal MVC Controller, but TestDbContext cannot be injected directly into a BackgroundService. Instead, you can inject the IServiceCopeFactory object, then call the IServiceCopeFactory.CreateScope () method to create an IServiceScope object when you use the TestDbContext object, and use the IServiceScope.ServiceProvider to manually get the TestDbContext object.

The code is as follows:

public class TestBgService:BackgroundService

{

private readonly IServiceScopeFactory scopeFactory;

public TestBgService(IServiceScopeFactory scopeFactory)

{

this.scopeFactory = scopeFactory;

}

protected override Task ExecuteAsync(CancellationToken stoppingToken)

{

using (var scope = scopeFactory.CreateScope())

{

var sp = scope.ServiceProvider;

var dbCtx = sp.GetRequiredService<TestDbContext>();

foreach (var b in dbCtx.Books)

{

Console.WriteLine(b.Title);

}

}

return Task.CompletedTask;

}

}

Secret three: Invoking asynchronous methods without “await”

When I was developing Youzack, which is a language learning website, there was a functionality that looks up words .In order to improve the response speed of the client, I saved the detailed information of each word to the file server in the form of “a JSON file for each word”. Therefore, when the client queries a word, it first goes to the file server to find out if there is a corresponding static file, and if there is, it directly loads the static file. If the word does not exist in the file server, then call the API method to query. After the API interface queries the word from the database, it will not only return the detailed information of the word to the client, but also upload the detailed information of the word to the file server. This will allow the client to query this word later, and it will be able to query directly from the file server.

So the operation of “query from the database to the words of detailed information and upload them to the file server” doesn’t make sense to the client, and it can reduce the response speed of the interface, so I just move the operation of “upload the file server” to asynchronous method, and not wait it using “await”.

The pseudocode is as follows:

public async Task<WordDetail> FindWord(string word)

{

var detail = await db.FindWordInDBAsync(word);

_=storage.UploadAsync($”{word}.json”,detail.ToJsonString());//upload without wait

return detail;

}

In the UploadAsync above, there is no await call, so as soon as it is queried from the database, the detail is returned to the client, leaving UploadAsync to execute in the asynchronous thread.

The “_=” is used to suppress compiler warnings for asynchronous methods that do not await.

Secret four: Don’t use Thread.Sleep in async method.

When writing code, there are times when we need to “pause for a while before we continue to execute the following code. “For example, if you call an HTTP interface but fails, you can wait two seconds and then try again.

In an asynchronous method, please use Task.Delay() instead of Thread.Sleep(), because Thread.Sleep() blocks the main thread and fails to achieve the purpose of “using asynchronism to increase system concurrency”.

The following code is incorrect:

public async Task<IActionResult> TestSleep()

{

await System.IO.File.ReadAllTextAsync(“d:/temp/words.txt”);

Console.WriteLine(“first done”);

Thread.Sleep(2000);

await System.IO.File.ReadAllTextAsync(“d:/temp/words.txt”);

Console.WriteLine(“second done”);

return Content(“xxxxxx”);

}

The above code will compile and execute correctly, but it will greatly reduce the system’s concurrency. So please use Task.Delay() instead of Thread.Sleep ().

The following is correct:

public async Task<IActionResult> TestSleep()

{

await System.IO.File.ReadAllTextAsync(“d:/temp/words.txt”);

Console.WriteLine(“first done”);

await Task.Delay(2000);//!!!

await System.IO.File.ReadAllTextAsync(“d:/temp/words.txt”);

Console.WriteLine(“second done”);

return Content(“xxxxxx”);

}

Secret five: How to use yield with async?

The yield keyword enables “pipelining” data processing by allowing the user of IEnumerable to process a piece of data as a result of producing a piece of data.

However, since both yield and async are syntactic sugars, the compiler compiles the methods into a class that uses the state machine. As a result, the two syntactic sugars meet and the compiler is confused, so yield can’t be used directly in an async method to return data.

So the following code is incorrect:

static async IEnumerable<int> ReadCC()

{

foreach (string line in await File.ReadAllLinesAsync(“d:/temp/words.txt”))

{

yield return line.Length;

}

}

So, please use IAsyncEnumerable instead of IEnumerable, the following code is correct:

static async IAsyncEnumerable<int> ReadCC()

{

foreach(string line in await File.ReadAllLinesAsync(“d:/temp/words.txt”))

{

yield return line.Length;

}

}

When calling the method with IAsyncEnumerable, do not using foreach+await, the following code is incorrect:

foreach (int i in await ReadCC())

{

Console.WriteLine(i);

}

The await keyword needs to be moved before the foreach, as follows is correct:

await foreach(int i in ReadCC())

{

Console.WriteLine(i);

}

The C# compiler is written by Microsoft and does not support “foreach (int i in await ReadCC())”, probably because it is compatible with the previous C# syntax specification.

--

--