This is a series of articles about Multi-threading.
Introduction
To avoid performance bottlenecks and enhance the overall responsiveness of your application, one can use asynchronous programming. However, traditional techniques for writing asynchronous applications can be complicated, making them difficult to write, debug, and maintain.
C# supports a simplified approach, async programming, that leverages asynchronous support in the .NET runtime. The compiler does the difficult work that the developer used to do, and your application retains a logical structure that resembles synchronous code. As a result, you get all the advantages of asynchronous programming with a fraction of the effort.
In the previous article, Multi-Threading (3), async, await in C#, we discussed the async and await keywords; they are the heart of async programming for C# to use an asynchronous method almost as easily as a synchronous method.
In this article, we will further discuss an async method associated with multi-await keyword, we examine the thread timing and spacing distributions.
The Content of the Article
- A - Introduction
- B - Multi await in an async Method
- C- Multi await and Task.Run
- D - Multi awaits and HttpClient.GetStringAsync Method
B- Multi await in an async Method
From the previous article, Multi-Threading (3), async, await in C#, we have the conclusion:
await in C#
- In the async method, once the keyword
await
is met for the first time
- First, the remaining task is actually executed by some random threads obtained from the runtime thread pool.
- Secondly, the calling method to the async method is not blocking the main thread; it will return to the first calling code that is not marked as await.
We examine the following async code.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleAppCore {
class Program {
static async Task Main() {
ConsoleWriteLine($ "Start Program");
Task < int > taskA = MethodAAsync();
for (int i = 0; i < 5; i++) {
ConsoleWriteLine($ " B{i}");
Task.Delay(50).Wait();
}
ConsoleWriteLine("Wait for taskA termination");
await taskA;
ConsoleWriteLine($ "The result of taskA is {taskA.Result}");
Console.ReadKey();
}
static async Task < int > MethodAAsync() {
for (int i = 0; i < 5; i++) {
ConsoleWriteLine($ " A{i}");
await Task.Delay(100);
}
int result = 123;
ConsoleWriteLine($ " A returns result {result}");
return result;
}
// Convenient helper to print colorful threadId on console
static void ConsoleWriteLine(string str) {
int threadId = Thread.CurrentThread.ManagedThreadId;
//Console.ForegroundColor = threadId == 1 ? ConsoleColor.White : ConsoleColor.Cyan;
switch (threadId) {
case 1:
Console.ForegroundColor = ConsoleColor.White;
break;
case 4:
Console.ForegroundColor = ConsoleColor.Cyan;
break;
case 5:
Console.ForegroundColor = ConsoleColor.Green;
break;
case 6:
Console.ForegroundColor = ConsoleColor.Red;
break;
case 7:
Console.ForegroundColor = ConsoleColor.Blue;
break;
case 8:
Console.ForegroundColor = ConsoleColor.Yellow;
break;
}
Console.WriteLine(
$ "{str}{new string(' ', 26 - str.Length)} Thread {threadId}");
}
}
}
Run the code; we have the output.
According to the output, we have the work flow.
- In the async method, MethodAAsync(), once the keyword
await
is met for the first time at Line 34
- First, the remaining task is actually executed by some random threads obtained from the runtime thread pool,
- i = 1: shown in light blue arrow;
- i = 2: shown in dark red arrow;
- i = 3: shown in dark blue arrow;
- i = 4: shown in the yellow arrow
- whike = 0: the workflow has not met await yet; it is still in the original thread, shown in the white arrow.
- finally, i = 4; last time await is met, a new thread is created, shown as dark blue, and continues to run to Line 38
- Secondly, the calling method to the async method is not blocking the main thread; it will return to the first calling code that is not marked as await at Line 15.
- This thread will run simultaneously with the new randomly created threads after the wait keyword. i.e.
- B2, B3 after the newly created thread A1;
- B4, after the newly created thread A2;
- "wait for taskA termination' at Line 21 after a new thread A3
- finally, after the async method, MethodAAsync(), done,
- the thread goes back to the original context at Line 25, shown in dark blue.
The following show the threads created randomly, but the pattern is exactly the same as we discussed above.
Choose the loop index at Lines 15 and 31 as i < 10.
Choose the loop index at Lines 15 and 31 as i < 20.
C - Multi await and Task.Run
Task.Run Method:
Queues the specified work to run on the ThreadPool and returns a task or Task<TResult> handle for that work.
We have discussed using the async keyword to create an async method, and the new thread is created after the await keyword is met. While we use async/await keywords associated with Task.Run method, the later's behavior is different from async/await keywords. By definition, Task.Run will create a new thread.
Let us see the following code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Async_Await
{
class Multi_Await4
{
// if not shown in Start up object, use this non async main
public static void Main(string[] args)
{
Test();
//Console.WriteLine(str);
Console.ReadLine();
}
public static async Task Test()
{
ConsoleWriteLine($"Starting {1000}");
Task task1 = StartTask(1000);
ConsoleWriteLine($"Starting {3000}");
Task task2 = StartTask(3000);
ConsoleWriteLine($"Starting {10000}");
Task task3 = StartTask(10000);
ConsoleWriteLine($"Starting {8000}");
Task task4 = StartTask(8000);
ConsoleWriteLine($"Starting {5000}");
Task task5 = StartTask(5000);
Task.WaitAll(task1, task2, task3, task4, task5);
// You will not get here until all tasks are finished (in 10 seconds)
Console.WriteLine("Done!");
}
private static Task StartTask(int timeToWait)
{
ConsoleWriteLine($"Enter StarTask {timeToWait}");
return Task.Run(async () =>
{
ConsoleWriteLine($"Waiting {timeToWait}");
await Task.Delay(timeToWait);
ConsoleWriteLine($"Done waiting {timeToWait}");
});
}
// Convenient helper to print colorful threadId on console
static void ConsoleWriteLine(string str)
{
int threadId = Thread.CurrentThread.ManagedThreadId;
switch (threadId)
{
case 1:
Console.ForegroundColor = ConsoleColor.White;
break;
case 4:
Console.ForegroundColor = ConsoleColor.Cyan;
break;
case 5:
Console.ForegroundColor = ConsoleColor.Green;
break;
case 6:
Console.ForegroundColor = ConsoleColor.Red;
break;
case 7:
Console.ForegroundColor = ConsoleColor.Blue;
break;
case 8:
Console.ForegroundColor = ConsoleColor.Yellow;
break;
case 9:
Console.ForegroundColor = ConsoleColor.DarkRed;
break;
case 10:
Console.ForegroundColor = ConsoleColor.Magenta;
break;
case 11:
Console.ForegroundColor = ConsoleColor.Gray;
break;
case 12:
Console.ForegroundColor = ConsoleColor.DarkCyan;
break;
}
Console.WriteLine(
$"{str}{new string(' ', 26 - str.Length)} Thread {threadId}");
}
}
}
Run the code; we have the output.
According to the output, we have the work flow.
In the code, we use a Task.Run method; within it, there is await keyword. Different from using C# async/await keywords to create an async method, we do not need to wait until the await keyword is met the first time to create a new thread and return the original thread back to the calling function. in fact, when Task.Run is call, it is in a new created thread, while the main thread is back to the calling function.
i.e. the behavior of Task.Run is exactly the same as await in an async method is met the first time:
- When Task.Run method is called
- First, the task is actually executed by some random threads obtained from the runtime thread pool.
- Secondly, the calling method for the Task.Run method is not blocking the main thread; it will return to the first calling code that is not marked as await.
The behavior is shown in the graph: when Task.Run is called at Line 46: (see the left side arrows)
- the first time: shown in red arrow;
- the second time: shown in light blue arrow;
- the third time: shown in dark red arrow;
- the fourth time: shown in green arrow
- the fifth time: shown in dark blue arrow.
while the await keyword is met, the same as the previous sample (see the right side arrows):
- the first time: shown in light blue arrow;
- the second time: shown in dark gray arrow;
- the third time: shown in dark red arrow;
- the fourth time: shown in red arrow;
- the fifth time: shown in dark gray arrow.
The tasks start close simultaneously and finish what they are doing in parallel, with the one taking the shortest time finishing first. This can be seen in the below image, where they start rather randomly but finish in order.
The following show the same pattern with different randomly created threads.
if we get rid of the await keyword, the thread created within the method will continue till the end of the method.
private static Task StartTask(int timeToWait)
{
ConsoleWriteLine($"Enter StarTask {timeToWait}");
return Task.Run(async () =>
{
ConsoleWriteLine($"Waiting {timeToWait}");
Task.Delay(timeToWait);
ConsoleWriteLine($"Done waiting {timeToWait}");
});
}
the thread created within the method will continue till the end of the method.
We change the code a little bit and run the Task.Run first, return it later, and add one line after the Task.Run method.
private static Task StartTask(int timeToWait)
{
ConsoleWriteLine($"Enter StarTask {timeToWait}");
var task = Task.Run( async () =>
{
ConsoleWriteLine($"Waiting {timeToWait}");
await Task.Delay(timeToWait);
ConsoleWriteLine($"Done waiting {timeToWait}");
});
ConsoleWriteLine($"Done waiting after {timeToWait}");
return task;
}
We can see Task.Run creates a new thread running its task, the main thread continue, instead of going back to the calling method as await keyword does; run the code; this is one of output.
This is the workflow.
The following show the same pattern with different randomly created threads.
D - Multi await and HttpClient.GetStringAsync Method
HttpClient.GetStringAsync Method
Send a GET request to the specified Uri and return the response body as a string in an asynchronous operation.
This method's behavior is the same as Task.Run; it will create a new thread and let the main thread just pass, not return back to the calling function.
Please see the code below.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
// https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/task-asynchronous-programming-model
// Modified, https://www.c-sharpcorner.com/article/multi-threading-3-1-async-multi-await/
namespace Async_Await
{
class Multi_Await5
{
// if not shown in Start up object, use this non async main
public static void Main(string[] args)
{
GetUrlContentLengthAsync();
ConsoleWriteLine($"Line {21}");
Console.ReadLine();
}
public static async Task<int> GetUrlContentLengthAsync()
{
ConsoleWriteLine($"Line {29}");
var client = new HttpClient();
Task<string> getStringTask =
client.GetStringAsync("https://learn.microsoft.com/dotnet");
ConsoleWriteLine($"Line {36}");
DoIndependentWork();
ConsoleWriteLine($"Line {40}");
string contents = await getStringTask;
ConsoleWriteLine($"Line {44}");
return contents.Length;
}
static void DoIndependentWork()
{
Console.WriteLine("Working...");
}
// Convenient helper to print colorful threadId on console
static void ConsoleWriteLine(string str)
{
int threadId = Thread.CurrentThread.ManagedThreadId;
//Console.ForegroundColor = threadId == 1 ? ConsoleColor.White : ConsoleColor.Cyan;
switch (threadId)
{
case 1:
Console.ForegroundColor = ConsoleColor.White;
break;
case 4:
Console.ForegroundColor = ConsoleColor.Cyan;
break;
case 5:
Console.ForegroundColor = ConsoleColor.Green;
break;
case 6:
Console.ForegroundColor = ConsoleColor.Red;
break;
case 7:
Console.ForegroundColor = ConsoleColor.Blue;
break;
case 8:
Console.ForegroundColor = ConsoleColor.Yellow;
break;
case 9:
Console.ForegroundColor = ConsoleColor.DarkRed;
break;
case 10:
Console.ForegroundColor = ConsoleColor.Magenta;
break;
case 11:
Console.ForegroundColor = ConsoleColor.Gray;
break;
case 12:
Console.ForegroundColor = ConsoleColor.DarkCyan;
break;
}
Console.WriteLine(
$"{str}{new string(' ', 26 - str.Length)} Thread {threadId}");
}
}
}
Run the code:
Workflow:
Two more for demo:
This is workflow, given by Microsoft:
Its behavior is the same as Task.Run, HttpClient.GetStringAsync method creates a new thread running its task, the main thread continue, instead of going back to the calling method as await key word does, Run the code, this is one of output.
References: