首页 > 其他分享 >.NET 异步

.NET 异步

时间:2022-10-23 11:23:21浏览次数:50  
标签:异步 Task await 任务 线程 密集型 NET

异步基础

所谓异步,对于计算密集型的任务,是以线程为基础的,而在具体使用中,使用线程池里面的线程还是新建独立线程,取决于具体的任务量;对于 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 的编码建

针对 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,await,任务,线程,密集型,NET
From: https://www.cnblogs.com/daboluo/p/16818167.html

相关文章