1. The principle of Task performing parallel tasks
The principle of using Task to perform parallel tasks is to divide the task into multiple small pieces, each small piece can be run on a different thread. Then, use the method to submit these small chunks to the thread pool as different tasks. The thread pool will automatically manage the creation and destruction of threads, and automatically adjust the number of threads according to the availability of system resources, thereby achieving the effect of maximizing the utilization of CPU resources.
2. 5 examples
Example 1
Here is a simple example showing how to use Task to perform parallel tasks:
void Task1() { // Create a task array var tasks = new Task[10]; for (var i = 0; i < ; i++) { var taskId = i + 1; // Submit task using method tasks[i] = (() => { ("Task {0} Run the thread {1} middle", taskId, ); // Execution task logic }); } // Wait for all tasks to be completed (tasks); ("All tasks are completed."); (); }
In this example, we create an array of tasks of length 10 and then submit each task to the thread pool using the method. When the task is executed, use attributes to obtain the ID of the current task and print it out for easy observation. Finally, we use the method to wait for all tasks to complete and print out a completion message.
Results of the run:
Task 3 Run thread 11
Task 4 Run thread 12
Task 8 Run thread 16
Task 1 Run thread 9
Task 2 Run thread 10
Task 5 Run thread 13
Task 6 Run thread 14
Task 7 Run thread 15
Task 9 Running thread 17
Task 10 Run thread 18
All tasks are completed.
It is worth noting that in actual development, the size and number of tasks need to be evaluated according to specific circumstances to ensure the efficiency and reliability of parallel tasks.
Example 2
Another example of using Task is to calculate the Fibonacci sequence. We can think of each item in the Fibonacci sequence as a task and then use the 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; // Calculate the first n terms of the Fibonacci sequence var tasks = new Task<long>[n]; for (var i = 0; i < n; i++) { var index = i; // When using loop variables in the closure, you need to assign a value to another variable if (i < 2) { tasks[i] = ((long)i); } else { tasks[i] = (() => Fib(index)); } } // Wait for all tasks to be completed (tasks); // Print result for (var i = 0; i < n; i++) { ("{0} ", tasks[i].Result); } (); }
In this example, we use a Task array to store all tasks. If the first two items need to be calculated, then use the creation to complete the task directly. Otherwise, use the method to create an asynchronous task and call the Fib method to calculate the result. After waiting for all tasks to complete, we iterate through the Task array and use the properties to get the results of each task and print it out.
Results of the run:
0 1 1 2 3 5 8 13 21 34
It should be noted that when creating an asynchronous task, since the value of a loop variable in the closure is uncertain, it needs to be assigned to another variable and use the variable within the closure. Otherwise, all tasks may use the value of the same loop variable, resulting in an error in the result.
Example 3
In addition to using Task arrays to store all tasks, you can also use methods to create parallel tasks. This method is similar to the method, both of which 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; // Calculate the number of factorials var task = (() => Factorial(n)); ("Computing factorial..."); // Wait for the task to be completed (); ("{0}! = {1}", n, ); (); }
In this example, we use the method to create an asynchronous task that calculates factorials and wait for the task to complete to print the result.
Running results:
Calculate factorial...
5! = 120
It should be noted that although both methods can create asynchronous tasks, they behave slightly differently. In particular, methods are always used as task schedulers, while methods can specify task schedulers, task types, and other options. Therefore, when choosing which method to use, it needs to be evaluated based on the situation.
Example 4
Another example of using Task is reading files asynchronously. In this example, we use the method to create a completion task and return the file contents as the result.
void Task4() { const string filePath = ""; var task = ((filePath)); // It's just convenient to give examples, the better code should be: (filePath); ("Read the file content..."); // Wait for the task to be completed (); ("File content: {0}", ); (); }
In this example, we use methods to create a completion task and read the file contents through the method and return it as the result. After waiting for the task to complete, we can get the result of the task by calling the attribute.
Please create notepad in the text as you like
It should be noted that in actual development, if you need to process large files or need to perform long 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, thereby improving application performance and responsiveness.
Example 5
The last example is to implement asynchronous tasks 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() { //Simulation time-consuming operation await ((3)); return "Finish"; } ($"{:yyyy-MM-dd HH:mm:}Start time-consuming operation..."); // Wait for the asynchronous method to complete var result = await LongOperationAsync(); ($"{:yyyy-MM-dd HH:mm:}Time-consuming operation completed: {result}"); (); }
In this example, we declare the LongOperationAsync method as an asynchronous method using the async/await keyword and wait for the 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 starts time-consuming operation...
2023-03-28 20:54:12.143 Time-consuming operation is completed: Completed
It should be noted that when using the async/await keyword, you should avoid using blocking threads inside the asynchronous method, otherwise it may cause the UI thread to be blocked. If a blocking operation must be performed, you can either put it on a different thread or use asynchronous IO operations to avoid blocking the thread.
3. Pay attention to using async/await keywords
When using the async/await keyword, there are still some details that need to be paid attention to, and two more examples are given.
Example 1
The sample code is as follows:
async Task Task6() { async Task<string> LongOperationAsync(int id) { //Simulation time-consuming operation await ((1 + id)); return $"{:}Finish {id}"; } ($"{:yyyy-MM-dd HH:mm:}Start time-consuming operation..."); // Wait for multiple asynchronous tasks to complete var task1 = LongOperationAsync(1); var task2 = LongOperationAsync(2); var task3 = LongOperationAsync(3); var results = await (task1, task2, task3); var resultStr = (",", results); ($"{:yyyy-MM-dd HH:mm:}耗时操作Finish: {resultStr}"); (); }
In this example, we use a 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 Start time-consuming operation...
2023-03-28 21:15:46.894 Time-consuming operation completed: 44.888 completed 1, 45.883 completed 2, 46.893 completed 3
Example 2
Another issue to note is thatasync/await
When using keywords, you should avoid them as much as possible.ConfigureAwait(false)
method. This method allows asynchronous operations to be restored to the originalSynchronizationContext
, thereby reducing the overhead of thread switching and improving performance.
However, in some cases, if the asynchronous operation is completed, it is necessary to return to the originalSynchronizationContext
On, useConfigureAwait(false)
This will cause the caller to fail to process the result correctly. Therefore, it is recommended that you only need to return to the originalSynchronizationContext
Used only when onConfigureAwait(false)
method.
Sample code: Suppose we have a console application with two asynchronous methods: MethodAAsync() and MethodBAsync(). MethodAAsync() will wait for 1 second and then return a string. MethodBAsync() will wait for 2 seconds and then return a string. The code looks like this:
async Task<string> MethodAAsync() { await (1000); return $"{:}>Hello"; } async Task<string> MethodBAsync() { await (2000); return $"{:}>World"; }
Now, we want to call both methods at the same time and merge their results into a string. We can write code like this:
async Task<string> CombineResultsAAsync() { var resultA = await MethodAAsync(); var resultB = await MethodBAsync(); return $"{:yyyy-MM-dd HH:mm:}: {resultA} | {resultB}"; }
This code looks very simple and clear, but it has a performance problem. When we call the CombineResultsAAsync() method, the first await operation will switch the execution context back to the original SynchronizationContext (i.e., the main thread), so our asynchronous operation will run on the UI thread. Since we have to wait 1 second before we can return the result from MethodAAsync(), the UI thread will be blocked until the asynchronous operation is completed and the result is available.
In this case, we can use the ConfigureAwait(false) method to specify the thread execution state that does not need to retain the current context, so that the asynchronous operation runs on a thread pool thread. This can be achieved through the following code:
async Task<string> CombineResultsBAsync() { var resultA = await MethodAAsync().ConfigureAwait(false); var resultB = await MethodBAsync().ConfigureAwait(false); return $"{:yyyy-MM-dd HH:mm:}: {resultA} | {resultB}"; }
By using the ConfigureAwait(false) method, we tell that asynchronous operations do not need to preserve the thread execution state of the current context, so that the asynchronous operations will run on a thread pool thread, not on a UI thread. Doing this avoids some potential performance issues, as our UI threads are not blocked, and asynchronous operations can run on a new thread pool thread.
4. Summary
When using the async/await keyword, some best practices should be followed to improve the readability, maintainability, and performance of your code. Here are some common best practices:
- Declare the asynchronous method as
Task
orTask<TResult>
type so that you can use the await keyword to wait for it to complete. If the asynchronous method returns nothing, it should be declared as Task type. - Avoid blocking thread operations as much as possible within an asynchronous method, and instead use non-blocking operations to simulate delays. If a blocking operation must be performed, you can either put it on a different thread or use asynchronous IO operations to avoid blocking the thread.
- Do not catch exceptions inside an asynchronous method and process them immediately, as this will make the code complicated and difficult to maintain. The caller should be allowed to handle the exception by itself. If exceptions must be caught inside an asynchronous method, they should also be wrapped as
AggregateException
Exception and pass it to the caller. - In use
ConfigureAwait(false)
Be careful when doing the method, only if you are sure that you do not need to return to the original oneSynchronizationContext
Use it only when on, otherwise the caller may not be able to process the result correctly. - Try to avoid using unsafe threading APIs in asynchronous methods, e.g.
or
etc. to ensure the portability and stability of the code. A non-blocking asynchronous approach should be used to simulate latency.
- When using the async/await keyword, some naming conventions should be followed, such as the name of the asynchronous method should be
Async
End to facilitate distinction between synchronous and asynchronous methods. - You can use it when you need to wait for multiple asynchronous tasks to complete at the same time
Method waits for all tasks to complete. If you only need to wait for one of the tasks to complete, you can use
Method waits for any task to complete.
- Inside an asynchronous method, time-consuming operations should be encapsulated as another asynchronous method and used where required
async/await
Keywords call them to improve the readability and maintainability of the code. - When using the async/await keyword, you should avoid using thread synchronization mechanisms as much as possible, e.g.
lock
Keyword orMonitor
class, because this will cause the UI thread to be blocked. Instead, asynchronous locking or other non-blocking thread synchronization mechanisms should be used.
In short, using Task and async/await can greatly simplify asynchronous programming and improve code readability, maintainability and performance. However, some details and best practices need to be paid attention to to ensure the correctness and stability of the code.
This is the end of this article about the examples of C#’s principle of using Task to implement parallel tasks. For more related content on C#’s parallel tasks, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!