SoFunction
Updated on 2025-03-07

Some points to pay attention to when programming C# asynchronously

Try not to write asynchronous methods with return value type void

In general, it is recommended that you do not write an asynchronous method with a return value type void, because doing so will break the convention between the starter of the method and the method itself. This set of conventions could have ensured that the main caller could catch the exceptions that occurred in the asynchronous method.

A normal asynchronous method reports exceptions through the Task object it returns. If an exception occurs during execution, the Task object enters a faulted state. When the main caller performs await operation on the Task object returned by the asynchronous method, if the object is in the faulted state, the system will throw the exception that occurs during the execution of the asynchronous method. On the contrary, if the task has not been executed to the place where the exception was thrown, the execution progress of the main caller will be paused in the await statement. The exception will only be thrown when the system arranges a thread to continue to execute the code below the statement.

To sum up, it is: when an exception occurs in a void's asynchronous method, the developer will not receive any notification, and the program will neither trigger ordinary exception handlers nor record these exceptions. In short, this will cause the relevant thread to terminate silently.

Don't combine synchronous methods with asynchronous methods

A method modified with the async keyword means that the method may return control to the main speaker before performing all work, and the return to the main speaker is a Task object representing the progress of the work. The principal can query the status of this object to see if the work has been completed, has not been completed, or has a failure during execution. In addition, this method also implies that the main tuner: the work performed by this method may take a long time, so it is recommended that you do some other things first and ask me for the results later.

On the contrary, if a method is designed as a synchronous method, it means that when the method is executed, its post-conditions must be met. No matter how long it takes to complete the work, it will use the same resources as the main speaker. The main speaker must wait until the method is fully implemented before it can be implemented downward.

Both methods are clearly written separately, but if they are combined together, it will make the method very difficult to use and may lead to various bugs, such as deadlocks. Therefore, two important principles are proposed here. First, don't let the synchronous method wait for the asynchronous method to be executed before it can be executed down (try not to use blocking methods such as Wait() and .result). Second, don't let the asynchronous method hand over the work that can be executed by itself, although it takes a long time and has a lot of calculations, to another asynchronous task for doing. '

Of course, for the second point, this does not mean that tasks with high computational volume should not be executed in a separate thread, but rather that tasks that can be quickly completed with only one thread should not be deliberately broken down into many smaller parts, and they should be executed on multiple new threads separately. Instead, the entire task should be handed over to a certain thread for execution.

When using asynchronous methods, try to avoid thread allocation as much as possible

Asynchronous tasks seem magical because this task is deliberately transferred to another place to do it, so that the asynchronous method to enable this task can continue to advance from the place where it was paused earlier after the task is completed. However, in order to play the role of an asynchronous task, it is necessary to ensure that handing over this task can indeed take up less resources, rather than just switching contexts between similar resources.

For example, for a console program, if it is just a task that is computationally expensive and time-consuming (or CPU-intensive task that runs for a long time), then it is not much advantage to put the task alone in another thread. Because doing so can only keep the worker thread busy all the time, and the main thread must be stuck there and wait for the worker thread to complete the task. In this case, two threads are actually used to complete the work that can be done by only one thread, which causes waste of resources.

Avoid unnecessary context switching

Currently, the asynchronous methods implemented in C# code using async and await are executed by default. The code after await is executed in the context captured earlier. This is because it is safer to do so. It will only cause a few unnecessary context switches at most without causing major errors in the program. On the contrary, if the system does not switch back to the Yamaguchi, then if you encounter code that can only be executed in a specific context, the program may crash. Therefore, whether there is no need to switch the context, the system will switch to the context captured earlier and put the statements after await in that context to execute.

If you do not want the system to make such arrangements, you can call the ConfigureAwait() method. This means that the following code does not need to be executed in the context captured earlier. For example, in many assembly structures, the code after the await statement is generally unrelated to the context. Therefore, the ConfigureAwait() method of the Task object can be called to tell the system that after executing this task, there is no need to run the code below await in the context captured earlier. As shown below:

public static async Task<XElement> ReadPacket(string url)
{
	var result=await DownloadAsync(url)
				.ConfigureAwait(false);
	return (result);			
}

C# language allows the program to execute all the statements below await in the context captured earlier. Although this is safer, it will reduce the efficiency of the program. Therefore, in order to enable users to use the program more smoothly, we should adjust the structure of the code, strip away the code that must be run in a specific context, and try to consider calling ConfigureAwait(false) in the await statement, so that the program can run the code below the statement in the default context instead of switching back to the previous context.

Asynchronous development through Task objects

Task is an abstract mechanism that can be used to represent a certain task, so the task can be handed over to other resources for completion. Task types and related classes and structures provide rich APIs, allowing developers to manipulate Task objects and the work represented by the object. In addition, the Task object itself also has some methods and properties that can be used to operate the tasks represented by this object. These Task objects can be combined to form a relatively large task, which can be executed in order and parallel.

The await statement can be used to ensure that certain tasks can be executed in a certain order, that is, the code below the statement can only be executed after the work that the statement wants to wait for.

In short, since C# provides a rich set of APIs, it is possible to write fairly elegant algorithms to process Task objects and arrange the tasks represented by these objects. The more thorough you understand the usage of tasks, the clearer the asynchronous code you write.

Here are two commonly used APIs:

WhenAll: A new task will be created based on the existing batch of tasks, and this new character can only be completed when all of the tasks are executed. Performing await operation on the returned new tasks will result in a list, and the execution results of those tasks are located in the list.

WhenAny: In order to obtain a result as soon as possible, multiple tasks may be initiated so that they can obtain the result from different channels. As long as one of the tasks is completed, your goal will be achieved. For this requirement, you can consider the use method and pass in the batch of tasks you have created. Await operation on the Task object returned by the WhenAny method can obtain a task, which refers to the task that was first executed in this batch of tasks.

Consider implementing a cancellation protocol for tasks

The programming model of asynchronous tasks (also called task-based asynchronous programming model) provides a standard API to cancel the execution progress of tasks or broadcast tasks. Although these APIs are optional, if a task can indeed report its progress or can be cancelled, you can consider implementing these APIs in a suitable way.

For tasks that need to be cancelled, we can use the CanclelationTokenSource object to cancel the operation. This kind of object is an object that plays a mediating role. The object is between the client code that is likely to issue a cancel request and the operation that supports the cancel function.

If the task being executed finds that the client wants to cancel the operation, it will throw a TaskCancledException through the ThrowIfCanclellationRequested() method, and the quack says that the entire workflow cannot be fully executed.

In addition, asynchronous methods with return value type void type should not support cancellation.

Caches the return value of a generic asynchronous method

Maybe when you are asynchronous programming, the return types you set for asynchronous methods are Task or Task<T>, but sometimes setting the return value type to Task may affect performance. If a loop or a certain piece of code needs to be run frequently, the system may allocate many Task objects, thereby occupying a lot of resources. Fortunately, C# provides a new type called ValueTask<T> object, which is more efficient than ordinary Tasks. This type is a value type, so when creating an object of this type, no additional space is required. This benefit allows us to create more such objects without worrying that it will occupy too much resources like a Task object. If your asynchronous method can directly return the corresponding value based on the results cached earlier, then you should especially consider setting the return value type to ValueTask<T>.

Secondly, ValueTask provides a constructor that can accept Task parameters, which will wait for the execution result of the Task within it.

Summarize

I have a lot of content shared today, and many of them are difficult to understand, but it is indeed a skill that must be mastered when writing high-performance asynchronous methods. Because the time is short, there is no time to tell it through the code, so you need to have a certain foundation to understand it, but I still hope it will be helpful to you.

The above is the detailed content of some points that need to be paid attention to in C# asynchronous programming. For more information about C# asynchronous programming, please pay attention to my other related articles!