【转】温故之.NET 异步

转自:https://zhuanlan.zhihu.com/p/38537169

这篇文章包含以下内容

  • 异步基础
  • 基于任务的异步模式
  • 部分 API 介绍

异步基础

所谓异步,对于计算密集型的任务,就是以线程为基础的多任务。而在具体使用中,使用线程池里面的线程还是新建独立线程,取决于具体的任务量;对于 I/O 密集型任务的异步,是以 Windows 事件为基础的。

.NET 提供了执行异步操作的三种方式:

  • 异步编程模型 (APM) 模式(也称 IAsyncResult 模式):在此模式中异步操作需要 Begin 和 End 方法(比如用于异步写入操作的 BeginWrite 和 EndWrite)。不建议新的开发使用此模式
  • 基于事件的异步模式 (EAP):这种模式需要一个或多个事件、事件处理程序委托类型和 EventArg 派生类型,以便在工作完成时触发。不建议新的开发使用这种模式
  • 基于任务的异步模式 (TAP):它是在 .NET 4 中引入的。C# 中的 async 和 await 关键字为 TAP 提供了语言支持。这是推荐使用方法

由于异步编程模型 (APM) 模式与基于事件的异步模式 (EAP)在新的开发中已经不推荐使用。故在此处我们就不介绍了,以下仅介绍基于任务的异步模式(TAP

基于任务的异步模式(TAP)

任务是工作的异步抽象,而不是线程的抽象。即当一个方法返回了 Task 或 Task<T>,我们不应该认为它一定创建了一个线程,而是开始了一个任务。这对于我们理解 TAP 是非常重要的。

TAP 以 Task 和 Task<T> 为基础。它把具体的任务抽象成了统一的使用方式。这样,不论是计算密集型任务,还是 I/O 密集型任务,我们都可以使用 async 、await 关键字来构建更加简洁易懂的代码

任务分为 计算密集型任务和 I/O密集型任务任务两种

  • 计算密集型任务:当我们 await 一个操作时,该操作会通过 Task.Run 方法启动一个线程来处理相关的工作
    工作量大的任务,通过为 Task.Factory.StartNew 指定 TaskCreateOptions.LongRunning选项 可以使新的任务运行于独立的线程上,而非使用线程池里面的线程
  • I/O 密集型任务:当我们 await 一个操作时,它将返回 一个 Task 或 Task
    值得注意的是,这儿并不会启动一个线程

虽然计算密集型任务和 I/O 密集型任务在使用方式上没有多大的区别,但其底层实现却大不相同。

那我们如何区分 I/O 密集型任务和计算密集型任务呢?

比如网络操作,需要从服务器下载我们所需的资源,它就是属于 I/O 密集型的操作;比如我们通过排序算法对一个数组排序时,这时的任务就是计算密集型任务。


简而言之,判断一个任务是计算型还是 I/O 型,就看它占用的 CPU 资源多,还是 I/O 资源多就可以了。

对于I/O密集型的应用,它们是以 Windows 事件为基础的,因此不需要新建一个线程或使用线程池里面的线程来执行具体工作。但我们仍然可以使用 asyncawait 来进行异步处理,这得益于 .Net 为我们提供了一个统一的使用方式: Task 或 Task<T>

举个例子,对于 I/O 密集型任务,使用方式如下

// 这是在 .NET 4.5 及以后推荐的网络请求方式
HttpClient httpClient = new HttpClient();
var result = await httpClient.GetStringAsync("https://www.baidu.com");// 而不是以下这种方式(虽然得到的结果相同,但性能却不一样,并且在.NET 4.5及以后都不推荐使用)
WebClient webClient = new WebClient();
var resultStr = Task.Run(() => {return webClient.DownloadString("https://www.baidu.com");
});

对于计算密集型应用,使用方式如下

Random random = new Random();
List<int> data = new List<int>();
for (int i = 0; i< 50000000; i++) {data.Add(random.Next(0, 100000));
}
// 这儿会启动一个线程,来执行排序这种计算型任务
await Task.Run(() => {data.Sort();
});

异步方法返回 Task 或 Task<TResult>,具体取决于相应方法返回的是 void 还是类型 TResult。如果返回的是 void,则使用 Task,如果是 TResult,则使用 Task<TResult>

不应该使用 out 或 ref 的方式来返回值,因为这可能产生意料之外的结果。因此,我们应该尽可能的使用 Task<TResult> 中的 TResult 来组合多个返回值

另外,await不能用在返回值为 void 的方法上,否则会有编译错误

针对 TAP 的编码建议

  • async 与 await 应该搭配使用。即它们要么都出现,要么都不出现
  • 仅在异步方法(即被 async 修饰的方法)中使用 await。否则会有编译器错误
  • 如果一个方法内部,没有使用 await,则该方法不应该使用 async 来修饰,否则会有编译器警告
  • 如果一个方法为异步方法(被 async 修饰),则它应该以 Async 结尾
  • 我们应该使用非阻塞的方式来编写等待任务结果的代码:
    使用 awaitawait Task.WhenAny、 await Task.WhenAllawait Task.Delay 去等待后台任务的结果。
    而不是 Task.Wait 、Task.ResultTask.WaitAnyTask.WaitAllThread.Sleep,因为这些方式会阻塞当前线程。
    即如果需要等待或暂停,我们应该使用 .NET 4.5 提供的 await 关键字,而不是使用 .NET 4.5 之前的版本提供的方式
  • 如果是计算密集型任务,则应该使用 Task.Run 来执行任务;如果是耗时比较长的任务,则应该使用 Task.Factory.StartNew 并指定 TaskCreateOptions.LongRunning 选项来执行任务
  • 如果是 I/O 密集型任务,不应该使用 Task.Run
    因为 Task.Run 会在一个单独的线程中运行(线程池或者新建一个独立线程),而对于 I/O 任务来说,启用一个线程意义不大,反而会浪费线程资源

创建任务

要创建一个计算密集型任务,在 .NET 4.5 及以后,可采用 Task.Run 的方式来快速创建;如果需要对任务有更多的控制权,则可以使用 .NET 4.0 提供的 Task.Factory.StartNew 来创建一个任务。
对于 I/O 密集型任务,我们可以通过将 await 作用于对应的 I/O 操作方法上即可

取消任务

在 TAP 中,任务是可以取消的。通过 CancellationTokenSource 来管理。需要支持取消的任务,必须持有 CancellationTokenSource.Token (令牌),以便该任务可以通过 CancellationTokenSource.Cancel() 的方式来取消。

使用 CancellationTokenSource 来取消任务,有以下优点

  • 可以将令牌传递给多个任务,这样可以同时取消多个任务。类似于一个老师,可以管理多个学生。
  • 可以通过 CancellationTokenSource.Token.Register 来监听任务的取消。这样我们可以在任务取消之后做一些其他的工作

任务处理进度

我们可以通过 IProgress<T> 接口监听进度,如下所示

public Task ReadAsync(byte[] buffer, int offset, int count, IProgress<long> progress)

在 .NET 4.5 提供单个 IProgress<T> 实现:Progress<T>Progress<T> 类的声明方式如下:

// Progress<T> 类的声明
public class Progress<T> : IProgress<T> {  public Progress();  public Progress(Action<T> handler);  protected virtual void OnReport(T value);  public event EventHandler<T> ProgressChanged;  
}   

举个例子,假设我们需要获取并显示下载进度,则可以按以下方式书写

private async void btnDownload_Click(object sender, RoutedEventArgs e) {  btnDownload.IsEnabled = false;  try {  txtResult.Text = await DownloadStringAsync(txtUrl.Text, new Progress<int>(p => pbDownloadProgress.Value = p));  }  finally { btnDownload.IsEnabled = true; }  
} 

部分 API 介绍

Task.WhenAll

此方法可以帮助我们同时等待多个任务,所有任务结束(正常结束、异常结束)后返回

这里需要注意的是,如果单个任务有异常产生,这些异常会合并到 AggregateException 中。我们可以通过 AggregateException.InnerExceptions 来得到异常列表;也可以使用 AggregateException.Handle 来对每个异常进行处理,示例代码如下

public static async void EmailAsync() {List<string> addrs = new List<string>();IEnumerable<Task> asyncOps = addrs.Select(addr => SendMailAsync(addr));try {await Task.WhenAll(asyncOps);} catch (AggregateException ex) {// 可以通过 InnerExceptions 来得到内部返回的异常var exceptions = ex.InnerExceptions;// 也可以使用 Handle 对每个异常进行处理ex.Handle(innerEx => {// 此处的演示仅仅为了说明 ex.Handle 可以对异常进行单独处理// 实际项目中不一定会抛出此异常if (innerEx is OperationCanceledException oce) {// 对 OperationCanceledException 进行单独的处理return true;} else if (innerEx is UnauthorizedAccessException uae) {// 对 UnauthorizedAccessException 进行单独处理return true;}return false;});}
}

但,如果我们需要对每个任务进行更加详细的管理,则可以使用以下方式来处理

public static async void EmailAsync() {List<string> addrs = new List<string>();IEnumerable<Task> asyncOps = addrs.Select(addr => SendMailAsync(addr));try {await Task.WhenAll(asyncOps);} catch (AggregateException ex) {// 此处可以针对每个任务进行更加具体的管理foreach (Task<string> task in asyncOps) {if (task.IsCanceled) {}else if (task.IsFaulted) {}else if (task.IsCompleted) {}}}
}

这样,就应该基本上足够应对我们工作中的大部分的异常处理了

Task.WhenAny

与 Task.WhenAll 不同,Task.WhenAny 返回的是已完成的任务(可能只是所有任务中的几个任务)

举个例子,比如我们开发了一个图片类App。我们可能需要在打开这个页面时,同时下载并展示多张图片。但我们希望无论是哪一张图片,只要下载完成,就展示出来,而不是所有的图片都下载完了之后再展示。示例代码如下

List<Task<Bitmap>> imageTasks = urls.Select(imgUrl => GetBitmapAsync(imgUrl)).ToList();
// 如果我们需要对图片做一些处理(比如灰度化),可以使用以下代码
// List<Task<Bitmap>> imageTasks = urls.Select(imgUrl => GetBitmapAsync(imgUrl).ContinueWith(task => ConvertToGray(task.Result)).ToList();
while(imageTasks.Count > 0) {  try {  Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);// 移除已经下载完成的任务imageTasks.Remove(imageTask);  // 同时将该任务的图片,在UI上呈现出来Bitmap image = await imageTask;  panel.AddImage(image);  } catch{}  
}

Task.Delay

此方法用于暂停当前任务的执行,在指定时间之后继续运行。

它可以与 Task.WhenAny 和 Task.WhenAll 结合,实现任务的超时,如下

public async void btnDownload_Click(object sender, EventArgs e) {  btnDownload.Enabled = false;  try {  Task<Bitmap> download = GetBitmapAsync(url); // 以下的这行代码表示,如果在 3s 之内没有下载完成,则认为超时if (download == await Task.WhenAny(download, Task.Delay(3000))) {  Bitmap bmp = await download;  pictureBox.Image = bmp;  status.Text = "Downloaded";  } else {  pictureBox.Image = null;  status.Text = "Timed out";  var ignored = download.ContinueWith(t => Trace("Task finally completed"));}  } finally { btnDownload.Enabled = true; }  
}  

通过这种方式,也可以监听使用 Task.WhenAll 时多个任务的超时,如下

Task<Bitmap[]> downloads = Task.WhenAll(from url in urls select GetBitmapAsync(url));  
if (downloads == await Task.WhenAny(downloads, Task.Delay(3000))) {  foreach(var bmp in downloads) panel.AddImage(bmp);  status.Text = "Downloaded";  
} else {status.Text = "Timed out";  downloads.ContinueWith(t => Log(t));  
}

另外,提供两个有用的函数,以方便我们在项目中使用

RetryOnFail

定义如下所示

// 如果下载资源失败后,我们希望重新下载时可以使用此方法
// 我们可以指定失败之后,间隔多长时间才重试。
// 也可以将 retryWhen 指定为 null,以便在失败之后立即重试
public static async Task<T> RetryOnFail<T>(Func<Task<T>> function, int maxTries, Func<Task> retryWhen) {for (int i = 0; i < maxTries; i++) {try {return await function().ConfigureAwait(false);} catch {if (i == maxTries - 1) throw;}if (retryWhen != null)await retryWhen().ConfigureAwait(false);}return default(T);
}

使用方式如下,这在失败之后,暂停 1s,然后再重试

string pageContents = await RetryOnFail(() => DownloadStringAsync(url), 3, () => Task.Delay(1000)); 

或者如下,这将在失败之后立即重试

string pageContents = await RetryOnFail(() => DownloadStringAsync(url), 3, null); 

NeedOnlyOne

定义如下

public static async Task<T> NeedOnlyOne<T>(params Func<CancellationToken, Task<T>>[] functions) {var cts = new CancellationTokenSource();var tasks = functions.Select(func => func(cts.Token));var completed = await Task.WhenAny(tasks).ConfigureAwait(false);cts.Cancel();foreach (var task in tasks) {var ignored = task.ContinueWith(t => Trace.WriteLine(t), TaskContinuationOptions.OnlyOnFaulted);}return await completed;
}

对于前面我们提到的下载电影的例子:获取到速度最快的渠道之后,立即取消其他的任务。现在我们可以这样做

var line = await NeedOnlyOne(token => DetectSpeedAsync("line_1", movieName, cts.Token),token => DetectSpeedAsync("line_2", movieName, cts.Token),token => DetectSpeedAsync("line_3", movieName, cts.Token));

以上提供的这两个方法,在实际项目中会非常有用,在需要时可以将它们用起来。当然,通过对 Task 的灵活运用,可以组合出更多方便的方法出来。在具体项目中多多使用即可

关于 Task 的一些基本的用法就介绍到这儿了

至此,本节内容讲解完毕。下一篇文章我们将讲解 .NET 中的并行编程。
欢迎关注公众号【嘿嘿的学习日记】,所有的文章,都会在公众号首发,Thank you~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/435862.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C库函数—strcpy实现

strcpy&#xff1a;将原串拷贝到目的串&#xff0c;不拷贝NULL 以下为具体实现&#xff1a; #include<stdio.h> #include<stdlib.h> #include<string.h> char * strcpy(char * strDest, const char * strSrc){ if(NULL strSrc || NULL strDest){ …

从数据库读写RadioButtonList选中的值

从数据库取出RadioButtonList选中的值 string str "体育";//这一项可以从数据库中读出 for (int j 0; j < this.RadioButtonList1.Items.Count; j) { if (str this.RadioButtonList1.Items[j].Text.ToString()) { this.RadioButtonList1.Items[j].Selected…

【转】DICOM中几个判断图像方向的tag

转自&#xff1a;https://www.cnblogs.com/h2zZhou/p/9072967.html 在DICOM标准里&#xff0c;有三个TAG与成像的方向相关。 参考来源&#xff1a;Kitware关于DICOM方向的说明 http://public.kitware.com/IGSTKWIKI/index.php/DICOM_data_orientation 包括 1、Image Positi…

贪心算法的几个应用

贪心算法具有2个性质&#xff1a; 1、贪心选择性质&#xff1a;只在当前状态下做最优选择&#xff0c;即局部最优选择&#xff0c;再自顶向下&#xff0c;去解做出这个选择后产生的相应子问题。每做一次选择&#xff0c;问题就转化为规模更小的子问题。对于一个具体问题&#x…

C#类和接口、虚方法和抽象方法及值类型和引用类型的区别

1.C#类和接口的区别 接口是负责功能的定义&#xff0c;项目中通过接口来规范类&#xff0c;操作类以及抽象类的概念&#xff01; 而类是负责功能的具体实现&#xff01; 在类中也有抽象类的定义&#xff0c;抽象类与接口的区别在于&#xff1a; 抽象类是一个不完全的类&#xf…

【转】DICOM医学图像读取涉及到的医学坐标体系

转&#xff1a;https://blog.csdn.net/sunyao_123/article/details/78975816 确定患者的位置和躺的方向主要有3个标签: (0018, 5100) Patient Position CS: ‘HFS’ (0020, 0032) Image Position (Patient) DS: [‘-167’, ‘-92’, ‘-28.5’] (0020, 0037) Image Orientatio…

学习 SQL 语句 - Select(4): 排序

Order By Asc|Desc本例效果图:代码文件:unit Unit1;interfaceusesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, StdCtrls, ExtCtrls, Grids, DBGrids, DB, ADODB;typeTForm1 class(TForm)DBGrid1: TDBGrid;DataSource1: TDataSource;…

二叉树总结一

二叉树的知识点1&#xff1a; 二叉树存储结构 前序建立二叉树 前序遍历、中序遍历、后序遍历&#xff08;递归、非递归&#xff09; 二叉树节点总数 二叉树叶子节点数 二叉树深度 遍历二叉树第i层节点 分层遍历二叉树&#xff08;递归、非递归&#xff09; 求二叉树中节点的最大…

【转】Ubuntu 16.04 远程桌面

转自&#xff1a;实现Windows直接远程访问Ubuntu 18.04&#xff08;旧版本也支持,无需安装第三方桌面,直接使用自带远程工具&#xff09; - 法号阿兴 - 博客园 一、设置Ubuntu 16.04 允许进行远程控制 首先&#xff0c;我们先设置Ubuntu的远程控制&#xff0c;将其设置为允许被…

GridView应用整理

2009年5月31日下午 21:58:38 获取GridView中RowCommand的当前选中行索引 <asp:GridView ID"BookGridView" runat"server"><Columns><asp:BoundField DataField"id" HeaderText"图书编号" /><asp:BoundField Data…

【转】VNC不同软件之间的联系与区别

转自&#xff1a;http://www.52iot.net/book/net/76.mhtml 如果不是经常远程不建议使用VNC&#xff0c;毕竟比较耗资源 不建议使用vnc&#xff0c; 如果只是使用文件服务&#xff0c;可以通过ssh协议的客户端访问过去操作。 现在gnome&#xff0c;kde, unity都是内置的&#x…

二叉树总结二

二叉树基本知识点2&#xff1a; 二叉树的非递归后序遍历 前序遍历是&#xff1a;根、左、右&#xff0c;后序遍历是&#xff1a;左、右、根&#xff0c;观察发现&#xff1a;前序和后序刚好遍历顺序相反。由于非递归后序遍历需要保存根节点及第几次访问&#xff0c;比较麻烦。可…

【转】Ubuntu下使用sysv-rc-conf管理服务

转自&#xff1a;https://www.linuxidc.com/Linux/2016-11/136834.htm sysv-rc-conf简介 sysv-rc-conf是一个强大的服务管理程序&#xff0c;群众的意见是sysv-rc-conf比chkconfig、rcconf好用。 背景知识 Ubuntu运行级别 Linux 系统任何时候都运行在一个指定的运行级上&…

几个常用SQL2000语句

排名语句select *,(select count(1) 1 from tPlanApply where applyquantity> a.applyquantity ) as 名次 from tPlanApply a order by 名次 替换语句 SQL code update tbset 字段replace(字段,替换后字符串,替换前字符串) 转载于:https://www.cnblogs.com/delphix/archiv…

WINCE6.0支持Multiple XIP

1. Multiple XIP support XIP(eXecute-In-Place)是本地执行&#xff0c;允许在ROM芯片内执行XIP区域(region)的应用代码&#xff0c;而不必再把代码读取到RAM中来执行。WINCE支持我们在单个系统中构建(construct)多个XIP区域&#xff0c;基于下面的理由使用多个XIP区域来代替…

【转】How to install VNC server on ubuntu 14.04

转自&#xff1a;https://www.krizna.com/ubuntu/install-vnc-server-ubuntu-14-04/ VNC server is used to share graphical desktop which can be controlled from other computers . This guide is helpful to install VNC server on Ubuntu Desktop 14.04, Ubuntu server …

Nsis 使用1-- 依条件显示自定义页面 custom page on condition

在制作安装程序的时候&#xff0c;我们会经常遇到根据用户的不同选择而随后显示不同的安装信息采集页面。 其实很简单&#xff0c;在使用NSIS的时候发现了这么个方法&#xff0c;记下来防止自己忘记哈&#xff1a;Code; MUI 2 compatible ------!include "MUI2.nsh"!…

WINCE6.0操作系统---内核(kernel)学习

备注&#xff1a;这里的内核指kernel&#xff0c;其表现形式为kernel.dll&#xff0c;OS指操作系统&#xff0c;core指核心。 图1 WINCE6.0体系结构 1. kernel的组成和功能 WINCE6.0操作系统内(kernel)在代码中的表现形式是kernel.dll(也就是kern.dll)&#xff0c; WINCE5…

【转】WPF调用图片路径,或资源图片

转自&#xff1a;https://www.cnblogs.com/sntetwt/p/5402098.html 一、加载本项目的图片 WPF引入了统一资源标识Uri(Unified Resource Identifier)来标识和访问资源。 其中较为常见的情况是用Uri加载图像。Uri表达式的一般形式为&#xff1a;协议授权路径 协议&#xff1a;pa…

如何自学java迅速成为java高手

很多网友咨询学习Java有没有什么捷径,我说“无他&#xff0c;唯手熟尔”。但是JAVA私塾愿意将一些经验写出来&#xff0c;以便后来者少走弯路,帮助别人是最大的快乐嘛&#xff01; 要想学好Java&#xff0c;首先要知道Java的大致分类。我们知道&#xff0c;自从Sun推出Java以来…