This article was completed through continuous conversations with ChatGPT, and the code was verified.
In C#, using Tasks makes it easy to perform parallel tasks. Task is a class that represents asynchronous operations that provides a simple, lightweight way to create multithreaded applications.
1. The principle of Task execution of parallel tasks
The principle of using Tasks to perform parallel tasks is to divide the task into multiple small chunks, each of which can be run on a different thread. You then use the Task.Run method to submit these small chunks to the thread pool as different tasks. The thread pool automatically manages the creation and destruction of threads, and automatically adjusts the number of threads based on the availability of system resources, thereby maximizing the use of CPU resources.
2. Five example displays
Examples 1
Here is a simple example of how to use Tasks to perform parallel tasks:
void Task1()
{
// 创建任务数组
var tasks = new Task[10];
for (var i = 0; i < tasks.Length; i++)
{
var taskId = i + 1;
// 使用Task.Run方法提交任务
tasks[i] = Task.Run(() =>
{
Console.WriteLine("任务 {0} 运行在线程 {1} 中", taskId, Task.CurrentId);
// 执行任务逻辑
});
}
// 等待所有任务完成
Task.WaitAll(tasks);
Console.WriteLine("所有任务运行完成。");
Console.ReadKey();
}
In this example, we create an array of tasks of length 10, and then use the Task.Run method to commit each task to the thread pool. When a task is executing, use the Task.CurrentId property to obtain the ID of the current task and print it out for easy observation. Finally, we use the Task.WaitAll method to wait for all tasks to complete and print out a completion message.
Results of the operation:
任务 3 运行在线程 11 中
任务 4 运行在线程 12 中
任务 8 运行在线程 16 中
任务 1 运行在线程 9 中
任务 2 运行在线程 10 中
任务 5 运行在线程 13 中
任务 6 运行在线程 14 中
任务 7 运行在线程 15 中
任务 9 运行在线程 17 中
任务 10 运行在线程 18 中
所有任务运行完成。
It is worth noting that in actual development, the size and number of tasks need to be evaluated according to specific situations to ensure the efficiency and reliability of parallel tasks.
Example 2
Another example of using Tasks is calculating the Fibonacci sequence. We can treat each item in the Fibonacci sequence as a task and use the Task.WaitAll method to wait for all tasks to complete.
void Task2()
{
static long Fib(int n)
{
if (n is 0 or 1)
{
return n;
}
else
{
return Fib(n - 1) + Fib(n - 2);
}
}
const int n = 10; // 计算斐波那契数列的前n项
var tasks = new Task<long>[n];
for (var i = 0; i < n; i++)
{
var index = i; // 需要在闭包内使用循环变量时需要赋值给另外一个变量
if (i < 2)
{
tasks[i] = Task.FromResult((long)i);
}
else
{
tasks[i] = Task.Run(() => Fib(index));
}
}
// 等待所有任务完成
Task.WaitAll(tasks);
// 打印结果
for (var i = 0; i < n; i++)
{
Console.Write("{0} ", tasks[i].Result);
}
Console.ReadKey();
}
In this example, we use a Task array to store all tasks. If the first two items need to be calculated, use Task.FromResult directly to create a completion task, otherwise use the Task.Run method to create an asynchronous task and call the Fib method to calculate the results. After waiting for all tasks to complete, we traverse the Task array and use the Task.Result property to retrieve and print the results for each task.
Results of the operation:
0 1 1 2 3 5 8 13 21 34
It should be noted that when creating an asynchronous task, since the value of the loop variable within the closure is uncertain, you need to assign it to another variable and use the variable within the closure. Otherwise, all tasks may use the value of the same loop variable, resulting in incorrect results.
Example 3
In addition to using a Task array to store all tasks, you can also use the Task.Factory.StartNew method to create parallel tasks. This method is similar to the Task.Run method in that it can create asynchronous tasks and submit them to the thread pool.
void Task3()
{
long Factorial(int n)
{
if (n == 0) return 1;
return n * Factorial(n - 1);
}
const int n = 5; // 计算阶乘的数
var task = Task.Factory.StartNew(() => Factorial(n));
Console.WriteLine("计算阶乘...");
// 等待任务完成
task.Wait();
Console.WriteLine("{0}! = {1}", n, task.Result);
Console.ReadKey();
}
In this example, we use the Task.Factory.StartNew method to create an asynchronous task that calculates factorial and wait until the task completes and print the results.
Operation results:
计算阶乘...
5! = 120
It should be noted that although both the Task.Run and Task.Factory.StartNew methods can create asynchronous tasks, their behavior is slightly different. In particular, the Task.Run method always uses TaskScheduler.Default as the task scheduler, while the Task.Factory.StartNew method can specify the task scheduler, task type, and other options. Therefore, when choosing which method to use, an evaluation needs to be made based on the specific situation.
Example 4
Another example of using Tasks is reading files asynchronously. In this example, we use the Task.FromResult method to create a completion task and return the file contents as the result.
void Task4()
{
const string filePath = "test.txt";
var task = Task.FromResult(File.ReadAllText(filePath)); // 只是方便举例,更好的代码应该是:File.ReadAllTextAsync(filePath);
Console.WriteLine("读取文件内容...");
// 等待任务完成
task.Wait();
Console.WriteLine("文件内容: {0}", task.Result);
Console.ReadKey();
}
In this example, we use the Task.FromResult method to create a completion task, and use the File.ReadAllText method to read the file contents and return it as a result. After waiting for the task to complete, we can get the result of the task by calling the Task.Result property.
Please create a notepad at will in this article. Here are the test results:
读取文件内容...
文件内容: Dotnet9,专注ASP.NET Core网站开发、MAUI跨平台应用开发、WPF客户端开发,同时以 https://Dotnet9.com 网站分享—些 技术类文章,欢迎交流学习。
It should be noted that in actual development, if you need to process large files or need to perform long-term I/O operations, you should use asynchronous code to avoid blocking UI threads. For example, when reading large files, we can use asynchronous code to avoid blocking UI threads, improving application performance and responsiveness.
Example 5
The final example is to implement an asynchronous task using Task and async/await. In this example, we encapsulate a time-consuming operation as an asynchronous method and use the async/await keyword to wait for the operation to complete.
async Task Task5()
{
async Task<string> LongOperationAsync()
{
// 模拟耗时操作
await Task.Delay(TimeSpan.FromSeconds(3));
return "完成";
}
Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}开始耗时操作...");
// 等待异步方法完成
var result = await LongOperationAsync();
Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}耗时操作完成: {result}");
Console.ReadKey();
}
In this example, we declare the LongOperationAsync method as asynchronous using the async/await keyword, and wait for the Task.Delay operation to complete using the await keyword. In the main program, we can use the await keyword to wait for LongOperationAsync to complete and get its results.
2023-03-28 20:54:09.111开始耗时操作...
2023-03-28 20:54:12.143耗时操作完成: 完成
It should be noted that when using the async/await keyword, you should avoid using thread-blocking operations within asynchronous methods, otherwise the UI thread may be blocked. If blocking operations must be performed, you can put them on a different thread or use asynchronous IO operations to avoid blocking threads.
3. Pay attention to using the async/await keyword
When using the async/await keyword, there are still some details to pay attention to, and two more examples will be given.
Examples 1
The example code is as follows:
async Task Task6()
{
async Task<string> LongOperationAsync(int id)
{
// 模拟耗时操作
await Task.Delay(TimeSpan.FromSeconds(1 + id));
return $"{DateTime.Now:ss.fff}完成 {id}";
}
Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}开始耗时操作...");
// 等待多个异步任务完成
var task1 = LongOperationAsync(1);
var task2 = LongOperationAsync(2);
var task3 = LongOperationAsync(3);
var results = await Task.WhenAll(task1, task2, task3);
var resultStr = string.Join(",", results);
Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}耗时操作完成: {resultStr}");
Console.ReadKey();
}
In this example, we use the Task.WhenAll method to wait for multiple asynchronous tasks to complete, and use the Join method to connect the results of all tasks as the final result.
2023-03-28 21:15:42.855开始耗时操作...
2023-03-28 21:15:46.894耗时操作完成: 44.888完成 1,45.883完成 2,46.893完成 3
Example 2
另一个需要注意的问题是,在使用async/await关键字时,应该尽可能避免使用ConfigureAwait(false)方法。这个方法可以让异步操作不必恢复到原始的SynchronizationContext上,从而减少线程切换的开销和提高性能。
然而,在某些情况下,如果在异步操作完成后需要返回到原始的SynchronizationContext上,使用ConfigureAwait(false)会导致调用者无法正确处理结果。因此,建议仅在确定不需要返回到原始的SynchronizationContext上时才使用ConfigureAwait(false)方法。
Sample code: Suppose we have a console application with two asynchronous methods: MethodAsync () and MethodBAsync(). MethodAsync () waits 1 second and then returns a string. MethodBAsync() waits 2 seconds and returns a string. The code is as follows:
async Task<string> MethodAAsync()
{
await Task.Delay(1000);
return $"{DateTime.Now:ss.fff}>Hello";
}
async Task<string> MethodBAsync()
{
await Task.Delay(2000);
return $"{DateTime.Now:ss.fff}>World";
}
Now, we want to call both methods at the same time and merge their results into a single string. We can write code like this:
async Task<string> CombineResultsAAsync()
{
var resultA = await MethodAAsync();
var resultB = await MethodBAsync();
return $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}: {resultA} | {resultB}";
}
This code may seem pretty straightforward, but it has a performance issue. When we call the CombineResultsAsync () method, the first await operation will switch the execution context back to the original SynchronizationContext (the main thread), so our asynchronous operation will run on the UI thread. Since we have to wait 1 second to return results from MethodAsync (), the UI thread will be blocked until the asynchronous operation completes and the results are available.
In this case, we can use the ConfigureAwait(false) method to specify that the thread execution state of the current context does not need to be preserved, allowing asynchronous operations to run on a thread pool thread. This can be achieved with the following code:
async Task<string> CombineResultsBAsync()
{
var resultA = await MethodAAsync().ConfigureAwait(false);
var resultB = await MethodBAsync().ConfigureAwait(false);
return $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}: {resultA} | {resultB}";
}
By using the ConfigureAwait(false) method, we tell the asynchronous operation that it does not need to preserve the thread execution state of the current context, so that the asynchronous operation will run on a thread pool thread rather than on a UI thread. Doing so avoids some potential performance issues because our UI threads are not blocked and asynchronous operations can run on a new thread pool thread.
IV. Summary
When using the async/await keyword, you should follow some best practices to improve the readability, maintainability, and performance of your code. Here are some common best practices:
尽可能将异步方法声明为
Task或Task<TResult>类型,以便可以使用await关键字等待其完成。如果异步方法不返回任何内容,则应将其声明为Task类型。Avoid thread-blocking operations within asynchronous methods whenever possible, and use non-blocking operations to simulate latency. If blocking operations must be performed, you can put them on a different thread or use asynchronous IO operations to avoid blocking threads.
在异步方法内部不要捕获异常并立即处理,因为这会导致代码变得复杂难以维护。应该让调用者自行处理异常。如果必须在异步方法内部捕获异常,也应该将其包装成
AggregateException异常,并将其传递给调用者。在使用
ConfigureAwait(false)方法时要小心,只有在确定不需要返回到原始的SynchronizationContext上时才使用,否则可能会导致调用者无法正确处理结果。尽量避免在异步方法中使用不安全的线程API,例如
Thread.Sleep或Thread.Join等方法,以确保代码的可移植性和稳定性。应该使用非阻塞的异步方法来模拟延迟。在使用async/await关键字时,应该遵循一些命名约定,例如异步方法的名称应该以
Async结尾,以便于区分同步和异步方法。在需要同时等待多个异步任务完成时,可以使用
Task.WhenAll方法等待所有任务完成。如果只需要等待其中一个任务完成,则可以使用Task.WhenAny方法等待任意一个任务完成。在异步方法内部,应该将耗时的操作封装为另外的异步方法,并在需要的地方使用
async/await关键字调用它们,以提高代码的可读性和可维护性。在使用async/await关键字时,应该尽可能避免使用线程同步机制,例如
lock关键字或Monitor类,因为这会导致UI线程被阻塞。而应该使用异步锁或其他非阻塞的线程同步机制。
In short, using Task and async/await can greatly simplify asynchronous programming and improve code readability, maintainability, and performance. However, there are some details and best practices that need to be noted to ensure the correctness and stability of the code.