C #In theAsynchronous/waitThe emergence of patterns has introduced new ways to write well-written and reliable parallel code, but as innovation continues to occur, it has also introduced many new ways. A lot of times when trying to useasync / awaitWhen solving multithreading problems, programmers not only don’t solve old problems, they create new ones, making them even harder to find when deadlocks, hunger, and race conditions still exist.
So I just want to share some of my experiences here. Maybe it will make someone’s life easier. But first, let’s take a brief history lesson on how the asynchronous/wait pattern emerged in our lives and what problems can be solved with it. It all starts with the callback, when we only have one function, as the second action, we pass the action, hereafter called. Like:
void Foo(Type parameter, Action callback) {... } void Bar() { some code here ... Foo(parameter, () => {... }); }Copy the code
That’s cool, but these pullback structures have a big growth trend.
Then there’s the Microsoft APM (asynchronous programming model), which has more or less similar problems with callbacks, exceptions, and still doesn’t know which thread code to execute in. The next step is to implement an asynchronous pattern based on EAP events. EAP introduces known asynchronous naming conventions, where the simplest class may only have a MethodName Async method and a corresponding MethodName Completed event, but it still feels a lot like a callback. This is interesting because it indicates that everything in the name that has asynchrony will now be returned to the task.
public class AsyncExample { // Synchronous methods. public int Method1(string param); public void Method2(double param); // Asynchronous methods. public void Method1Async(string param); public void Method1Async(string param, object userState); public event Method1CompletedEventHandler Method1Completed; public bool IsBusy { get; }}Copy the code
Finally, let’s talk about TAP or task asynchronous patterns that we all love and know about, right? Asynchronous methods in TAP contain a suffix after the Async operation name that is used to return wait type methods, such as Task, task.result, ValueTask, and ValueTaskResult. In this way, we introduce a Task object, which gives us many advantages over the above pattern.
- Code execution status, for example
Cancelled,
Faulted
.RanToCompletion - Clear mission cancelled
CancellationToken
TaskScheduler
This helps with the code execution context
Now, with a brief history, let’s jump to some more practical things that could make our lives a little easier. I’m going to try and mention some of the most important practices THAT I use the most.
I often hear from colleagues and read in posts that you are having deadlock problems and can. ConfigureAwait(false) use it anywhere and you’ll be fine, I really disagree. While using.configureAwait (false) can indeed be beneficial, it is always important to keep certain things in mind. For example, ConfigureAwait should not be used in methods that require context when there is code after waiting. The CLR controls which thread code will be executed after the await keyword, and. ConfigureAwait(false) We are basically saying that we don’t care which thread code executes after the await keyword. This means that if we operate using UI or in ASP.Net, we can use HttpContext.current or build an HTTP response, and we always need to continue executing in the main thread. However, if you’re writing a library and aren’t sure how to use it, it’s a good idea to use it. ConfigureAwait(false)- Used with ConfigureAwait, a small amount of parallel processing can be achieved: some asynchronous code can run the main thread in parallel, rather than constantly adding work to it.
CancellationToken CancellationToken In general, it is a good idea to use a class with a task. This mechanism is a handy utility for controlling the flow of task execution, especially for long execution methods that may be stopped by the user. This could be heavy computing, a long-running database request, or just a network request.
public async Task MakeACallClickAsync(CancellationToken cancellationToken)
{
try
{
var result = await GetResultAsync(cancellationToken);
}
catch (OperationCanceledException) // includes TaskCanceledException
{
MessageBox.Show("Your operation was canceled.");
}
}
public async Task<MyResult> GetResultAsync(CancellationToken cancellationToken)
{
try
{
return await httpClient.SendAsync(httpRequestMessage, cancellationToken);
}
catch (OperationCanceledException)
{
// perform your cleanup if necessary
}
}
Copy the code
Please note that there are two cancel exception types: TaskCanceledException and OperationCanceledException. TaskCanceledException derived from OperationCanceledException. Therefore, when writing for handling cancelled operation failure of the catch block, best capture OperationCanceledException, otherwise some cancellation event will leak if the hole in your catch block and lead to unpredictable results.
.result/Wait() Using this method is very simple – try to avoid it unless you are 100% sure of your work but still so await it. Microsoft says that this Wait(TimeSpan) is a synchronous method that causes the calling thread to Wait for the current task instance to complete. Remember when we mentioned that tasks are always executed in context, and that CLR controls execute thread after thread in context? Look at the following code:
// Service method.
public async Task<JsonResult>GetJsonAsync(Uri uri) { var client = _httpClientFactory.CreateClient(); var stream = await _httpClient.GetStreamAsync("https://really-huge.json"); using var sr = new StreamReader(stream); using var reader = new JsonTextReader(sr); while (reader.Read()) { .... get json result } return jsonResult; } // Controller method. public class MyController : Controller { [HttpGet] public string Get() { var jsonTask = GetJsonAsync(Uri.Parse("https://somesource.com")); return jsonTask.Result.ToString(); }}Copy the code
- In the controller, we call it GetJsonAsync (ASP.NET Context).
- HTTP request _httpClient. GetStreamAsync (” HTTPS / / really – huge. Json “) is started
- GetStreamAsync then returns the unfinished task indicating that the request was not completed.
- GetJsonAsync waits for the task returned by GetStreamAsync. The context is saved and will be used to continue running the GetJsonAsync method. GetJsonAsync returns an unfinished Task indicating that the GetJsonAsync method has not completed.
- through
jsonTask.Result.ToString()
; Used in the controller, we can synchronize tasks that prevent GetJsonAsync from returning. This will block the main upper context thread. - At some point, GetStreamAsync will complete and its Task will complete, after which time GetJsonAsync is ready to continue, but it waits for the context to be available so it can execute in the context. Then we have a deadlock because the controller method blocks the context thread while waiting for *GetJsonAsync to complete, while GetJsonAsync waits for the context to be available so it can complete.
Such deadlocks are not easy to detect and often cause a lot of discomfort, which is why Wait () and are not recommended. As a Result.
Task.yield () To be honest, this is a little trick that I don’t use much, but having your Arsenal is a good thing. The problem is that when async/await is used, there is no guarantee that you will run asynchronously when await FooAsync() is actually used. The internal implementation is free to return using a fully synchronized path. Suppose we have some methods:
async Task MyMethodAsync() {
someSynchronousCode();
await AnotherMethodAsync();
continuationCode();
}
Copy the code
It looks like we’re not blocking anything here and want to await AnotherMethodAsync(); It can also run asynchronously, but let’s take a look behind the scenes. When our code is compiled, we get something like this (very simplified) :
async Task MyMethodAsync() { someSynchronousCode(); var awaiter = AnotherMethodAsync().GetAwaiter(); if (! awaiter.isCompleted) { compilerLogic(continuationCode()); // asynchronous code } else { continuationCode(); // synchronous code } }Copy the code
Here’s what happened:
- OmeSynchronousCode () will run synchronously as expected.
- then
AnotherMethodAsync()
Is synchronous execution, then we get theta. GetAwaiter An awaiter object () - We can see if the task is complete by checking waiter.IsCompleted
- If the task is complete, then we can simply run continuationCode () simultaneously
- ContinuationCode () plans to execute in the task context if the task is not completed
As a result, even await can still execute code synchronously, and by using code, await task.yield () we can always guarantee! Await. IsCompleted and forces the method to complete asynchronously. Sometimes it can make a difference, such as in the UI thread, where we can make sure we’re not busy for long periods of time.
ContinueWith()
Often, after one operation completes, we need to call a second operation and pass data to it. Traditionally, the callback method is used to run continuations. In the task parallel library, continuation tasks provide the same functionality. Multiple overloaded ContinueWith exposed by the Task type. This method creates a new task that will be scheduled when another task completes. Let’s take a look at a very common situation where we are downloading images and doing some processing to each image. We need to process it sequentially, but we want as many concurrent downloads as possible, and ThreadPool performs computationally intensive processing on the downloaded images:
List<Task<Bitmap>> imageTasks = urls.Select(u =>
GetBitmapAsync(imageUrl)
.ContinueWith((t) => {
if (t.Status == TaskStatus.RanToCompletion) {
ConvertImage(t.Result)
}
else if (t.Status == TaskStatus.Faulted) {
_logger.Log(t.Exception.GetBaseException().Message);
}
})).ToList();
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap>imageTask = await Task.WhenAny(imageTasks); imageTasks.Remove(imageTask); Bitmap image = await imageTask; . add orr store the image ..... . .AddImage(image); } catch{} }Copy the code
ContinueWith() is very handy in this case because, unlike the code that follows, ContinueWith() logic will be executed on the thread pool, which is what we need to do the calculation and prevent the main thread from blocking. Note that task.continueWith passes a reference to the previous object to the user delegate. If the previous System. The object is Threading. The Tasks. The Task object, and the Task to run, can perform a Task Task. The Result attribute. In fact, the.result property blocks until the Task is complete, but ContinueWith() is also called by another Task when the Task state changes. This is why we check the status first before processing or logging errors.
Related to the original
Originally published by Eduard Los https://medium.com/@eddyf1xxxer/bi-directional-streaming-and-introduction-to-grpc-on-asp-net-core-3-0-part-2-d9127a58dcd bCopy the code