C#中异步/等待模式的出现引入了编写良好且可靠的并行代码的新方法,但是,随着创新不断发生,它也引入了将许多的的新方法。很多时候,当尝试使用async / await解决多线程问题时,程序员不仅不解决旧问题,还创建新的问题,当死锁,饥饿和竞争条件仍然存在时,甚至更难找到它们。
所以我只是想在这里分享我的一些经验。也许它将使某人的生活更轻松。但是首先,让我们先学习一个简短的历史课程,看看异步/等待模式是如何在我们的生活中出现的,以及借助它可以解决哪些问题。这一切都从回调开始,当我们只有一个函数时,作为第二个动作,我们传递动作,此后称为。就像是:
void Foo(Type parameter, Action callback) {...}
void Bar() {
some code here
...
Foo(parameter, () => {...});
}
这很酷,但是这些回调结构有很大的增长趋势。
然后是Microsoft APM(异步编程模型),它在回调,异常方面也或多或少存在类似问题,并且仍不清楚在哪个线程代码中执行。下一步是实现基于EAP事件的异步模式。EAP引入了已知的异步命名约定,其中最简单的类可能只有一个MethodName Async方法和一个相应的MethodName Completed事件,但仍然感觉很像回调。这很有趣,因为它表明现在名称中具有异步的所有内容都将返回任务。
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; }
}
最后,我们来谈谈我们都喜欢并知道的TAP或任务异步模式,对吗?TAP中的异步方法在Async操作名称之后包含后缀,该后缀用于返回等待类型的方法,例如Task,Task.Result,ValueTask和ValueTaskResult。通过这种方法,引入了一个Task对象,与上面的模式相比,它给我们带来了很多好处。
- 代码执行状态,例如
Cancelled,
Faulted
, RanToCompletion - 明确的任务取消
CancellationToken
TaskScheduler
这有助于代码执行上下文
现在,通过简短的历史介绍,让我们跳到一些更实际的事情,这些事情可以使我们的生活更轻松一些。我将尝试并提及一些我最常用的最重要的做法。
.ConfigureAwait(假)
我经常从同事那里听到消息,也经常在帖子中读到,您遇到了死锁问题,可以.ConfigureAwait(false)
在任何地方使用它,您会没事的,我真的不同意。尽管使用.ConfigureAwait(false)
可能确实有益,但牢记某些事情始终很重要。例如,ConfigureAwait
在需要上下文的方法中,在等待之后有代码时,不应使用。CLR控制将在await关键字之后执行哪个线程代码,而.ConfigureAwait(false)
我们基本上是说我们不在乎在await关键字之后执行哪个线程代码。这意味着,如果我们使用UI或在ASP.Net中进行操作,则可以使用HttpContext.Current
或构建http响应,我们始终需要在主线程中继续执行。但是,如果您正在编写一个库,但不确定如何使用该库,则使用它是一个好主意.ConfigureAwait(false)
-通过ConfigureAwait
这种方式使用,可以实现少量的并行处理:某些异步代码可以并行运行主线程,而不是不断地给它添加一些工作要做。
CancellationToken
CancellationToken
通常,将类与任务一起使用是一个好主意,此机制是控制任务执行流程的简便实用工具,特别适用于可能被用户停止的长执行方法。这可能是繁重的计算过程,长时间运行的数据库请求或仅仅是网络请求。
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
}
}
请注意,有两种取消异常类型:TaskCanceledException和OperationCanceledException。TaskCanceledException
派生自OperationCanceledException
。因此,在编写用于处理已取消操作的失败的catch块时,最好捕获OperationCanceledException
,否则某些取消事件会在您的catch块中漏出并导致无法预测的结果。
.Result / Wait()
使用这种方法非常简单-尽量避免使用它,除非您对自己的工作有100%的把握,但仍然如此await
。微软表示,这Wait(TimeSpan)是一种同步方法,可使调用线程等待当前任务实例完成。
还记得我们提到过,任务总是在上下文中执行的,而clr控件将在其中执行线程连续吗?看下面的代码:
// 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();
}
}
- 在控制器中,我们称为GetJsonAsync(ASP.NET上下文)。
- http请求_httpClient.GetStreamAsync("https//really-huge.json")已启动
- 然后GetStreamAsync返回未完成的任务,指示请求未完成。
- GetJsonAsync等待GetStreamAsync返回的任务。上下文已保存,将用于继续运行GetJsonAsync方法。GetJsonAsync返回未完成的Task,指示GetJsonAsync方法尚未完成。
- 通过
jsonTask.Result.ToString()
;在控制器中使用,我们可以同步阻止GetJsonAsync返回的Task。这将阻塞主上下文线程。 - 在某个时刻,GetStreamAsync将完成并且其Task将完成,在此之后,GetJsonAsync准备好继续,但是它等待上下文可用,以便可以在上下文中执行。然后我们有一个死锁,因为控制器方法在等待*GetJsonAsync完成的同时阻塞了上下文线程,而GetJsonAsync在等待上下文可用以便可以完成。
这种死锁不容易发现,并且经常会引起很多不适,这就是为什么不建议使用Wait()和.Result的原因。
Task.Yield()
老实说,这是一个小技巧,我没用太多,但是拥有您的武器库是一件好事。这个问题是,当使用async/await
,并不能保证您await FooAsync()
实际使用时会异步运行。内部实现可以使用完全同步的路径自由返回。假设我们有一些方法:
async Task MyMethodAsync() {
someSynchronousCode();
await AnotherMethodAsync();
continuationCode();
}
看起来我们没有在这里阻塞任何东西,并且希望await AnotherMethodAsync()
;也可以异步运行,但是让我们看一下幕后会发生什么。当我们的代码被编译时,我们可以得到类似于下面的代码(非常简化):
async Task MyMethodAsync() {
someSynchronousCode();
var awaiter = AnotherMethodAsync().GetAwaiter();
if (!awaiter.isCompleted)
{
compilerLogic(continuationCode()); // asynchronous code
}
else
{
continuationCode(); // synchronous code
}
}
这是发生了什么:
- omeSynchronousCode()将按预期同步运行。
- 然后
AnotherMethodAsync()
是同步执行,那么我们得到与.GetAwaiter一个awaiter对象() - 通过检查waiter.IsCompleted,我们可以查看任务是否完成
- 如果任务完成,那么我们只需同步运行continuationCode()
- 如果任务未完成,则continuationCode()计划在任务上下文中执行
结果,即使await
仍然可以同步执行代码,并且通过使用代码,await Task.Yield()
我们始终可以保证!awaiter.IsCompleted
并强制该方法异步完成。有时它可以有所作为,例如在UI线程中,我们可以确保我们长时间不忙。
ContinueWith()
常见的情况是,一个操作完成后,我们需要调用第二个操作并将数据传递给它。传统上,回调方法用于运行延续。在任务并行库中,延续任务提供了相同的功能。Task类型公开的多个重载ContinueWith
。此方法创建一个新任务,该任务将在另一个任务完成时进行安排。让我们看一下一个非常常见的情况,我们正在下载图像并对每个图像进行一些处理。我们需要按顺序进行处理,但是我们希望尽可能多的并发下载,而且还要ThreadPool
对下载的图像执行计算密集型处理:
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{}
}
在这种情况下,使用起来非常方便,ContinueWith()
因为与之后的代码不同await
,ContinueWith()
逻辑将在线程池上执行,这是我们进行计算并防止主线程阻塞所需要的。
请注意,Task.ContinueWith 将对上一个对象的引用传递给用户委托。如果先前的对象是System.Threading.Tasks.TaskContinueWith()
当任务状态更改时,也会由另一个Task调用。这就是为什么我们首先检查状态,然后才进行处理或记录错误的原因。
原文相关
原文作者:Eduard Los
原文地址:https://medium.com/@eddyf1xxxer/bi-directional-streaming-and-introduction-to-grpc-on-asp-net-core-3-0-part-2-d9127a58dcdb