C# NET asynchrony, five uses you may not know

Posted by Shit4Brains on Fri, 04 Mar 2022 14:36:12 +0100

C# NET asynchrony, five uses you may not know

async/await asynchronous operation is a very amazing "syntax sugar" in C#, which makes asynchronous programming beautiful and stupid to an incredible extent. Even JavaScript uses async/await syntax for reference, which makes the JavaScript code overflowing with callbacks beautiful.

Usage 1. Control the number of tasks executed in parallel
During project development, sometimes many tasks need to be executed asynchronously, but in order to avoid too many asynchronous tasks being executed at the same time, which will reduce the performance, it is usually necessary to limit the number of tasks executed in parallel. For example, when a crawler grabs content from the Internet in parallel, it must limit the maximum number of threads to execute according to the situation.

In the era without async/await, semaphores and other mechanisms need to be used for inter thread communication to coordinate the execution of each thread. Developers need to know the technical details of multithreading very well. After using async/await, all this can become very stupid.

For example, the following code is used to start with words Txt, in each dictionary of a English word, read the word one by one, then call a API interface to get the detailed information of the word "phonogram, Chinese meaning, example sentence". In order to speed up the processing speed, asynchronous programming is needed to realize simultaneous downloading of multiple tasks, but the number of tasks executed at the same time is limited (assumed to be 5). The implementation 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 paragraph:

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();
       }
}

Here, we traverse all the words, grab the words and save the return value of the Process method to the disk. The Task does not use the await keyword to decorate, but saves the returned Task object to the list. Because we do not use await to wait, we can add the next Task to the list without waiting for the completion of one Task. When there are five tasks in the list, await Task is called WhenAll(tasks); Wait for the completion of these five tasks before processing the next group (5). Await Task outside the loop WhenAll(tasks); Is used to deal with the last group of less than five tasks.

Usage 2. Inject DI into asynchronously executed code such as BackgroundService

When using dependency injection (DI), the injected objects have a life cycle. For example, use services AddDbContext(…); DbContext is injected into the core of testcontext in this way. TestDbContext can be directly injected into the Controller of ordinary MVC, but TestDbContext cannot be directly injected into BackgroundService. At this time, you can inject the IServiceScopeFactory object, then call the CreateScope() method of IServiceScopeFactory to generate an IServiceScope when using the TestDbContext object, and use the ServiceProvider of IServiceScope to manually parse and obtain 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;
       }
}

Usage 3. Asynchronous methods can be used without await

When I memorize words in youzack, I have a function of querying words. In order to improve the response speed of the client, I saved the details of each word to the file server in the form of "one json file for each word", which is equivalent to a "static". Therefore, when querying words, the client first goes to the file server to find out whether there is a corresponding static file. If so, it will load the static file directly. If the file server does not exist, call the API interface method to query. After the API interface queries the word from the database, it will not only return the word details to the client, but also upload the word details to the file server. In this way, the client can query this word directly from the file server in the future.

Therefore, the operation of "uploading the detailed information of words queried from the database to the file server" in the API interface is meaningless to the requester of the interface and will reduce the response speed of the interface. Therefore, I wrote the operation of "uploading to the file server" into the asynchronous method and did not wait through await.

The pseudo code is as follows:

public async Task<WordDetail> FindWord(string word)
{
       var detail = await db.FindWordInDBAsync(word);//Query from database
       _=storage.UploadAsync($"{word}.json",detail.ToJsonString());//Upload to file server, but don't wait
       return detail;
}

In the above UploadAsync call, there is no await call waiting, so as long as it is queried from the database, the detail will be returned to the requester, leaving UploadAsync to execute slowly in the asynchronous thread.

The preceding "=" is to eliminate the compiler warning caused by non await asynchronous methods.

Usage 4. Sleep pit in asynchronous code

When writing code, sometimes we need to "pause for a while before continuing to execute the code". For example, call an Http interface. If the call fails, you need to wait 2 seconds and try again.

In asynchronous methods, if you need to "pause for a period of time", use task Delay() instead of thread Sleep(), because thread Sleep () will block the main thread, which will not achieve the purpose of "using asynchrony to improve the concurrency of the system".

The following code is wrong:

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 can be compiled and executed correctly, but it will greatly reduce the concurrent processing ability of the system. So use task Delay() replaces thread Sleep(). The following is true:

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");
}

Usage 5. How to use yield in asynchronous methods

yield can realize "let IEnumerable users process a data when a data is generated", so as to realize the "pipelining" of data processing and improve the speed of data processing.

However, since yield and async are syntax sugars provided by the compiler, the compiler will compile their modified methods into a class using state machine. Therefore, when the two syntax sugars meet, the compiler is confused, so it can not directly use yield to return data in async modified asynchronous methods.

Therefore, the following code is wrong:

static async IEnumerable<int> ReadCC()
{
       foreach (string line in await File.ReadAllLinesAsync("d:/temp/words.txt"))
       {
              yield return line.Length;
       }
}

Just change IEnumerable to IAsyncEnumerable. The following is correct:

static async IAsyncEnumerable<int> ReadCC()
{
       foreach(string line in await File.ReadAllLinesAsync("d:/temp/words.txt"))
       {
              yield return line.Length;
       }
}

However, when calling the code that uses async and yield at the same time, the normal foreach+await cannot be used. The following is an error:

foreach (int i in await ReadCC())
{
       Console.WriteLine(i);
}

You need to move the await keyword before foreach. The following is correct:

await foreach(int i in ReadCC())
{
       Console.WriteLine(i);
}

The compiler was written by Microsoft. I don't know why it doesn't support foreach (int i in await ReadCC()). It may be because it has to be compatible with the previous C# syntax specification.

Topics: C#