调用博主最近登录时间
生活中的HYGGE
搞明白C#的Async和Await

搞明白C#的Async和Await

hygge
2022-07-25 / 0 评论 / 302 阅读 / 正在检测是否收录...

本文参考: bilibili视频

讲的非常棒!

需求引入 - 下载数据

从不同的网站中下载很大的数据量,并且将数据结果呈现在UI客户端的一个经典的实际场景

界面

l60lh7ay.png

代码

public partial class Form1 : Form
{
    private readonly HttpClient httpClient = new HttpClient();

    public Form1()
    {
        InitializeComponent();
    }
}

界面中有一个同步下载按钮(SyncDownload)、异步下载按钮(AsyncDownload)、信息输出的文本框

常量类

public class Contents
{
    public static readonly IEnumerable<string> WebSites = new string[]
    {
        "https://www.zhihu.com",
        "https://www.baidu.com",
        "https://www.weibo.com",
        "https://www.stackoverflow.com",
        "https://docs.microsoft.com",
        "https://docs.microsoft.com/aspnet/core",
        "https://docs.microsoft.com/azure",
        "https://docs.microsoft.com/azure/devops",
        "https://docs.microsoft.com/dotnet",
        "https://docs.microsoft.com/dynamics365",
        "https://docs.microsoft.com/education",
        "https://docs.microsoft.com/enterprise-mobility-security",
        "https://docs.microsoft.com/gaming",
        "https://docs.microsoft.com/graph",
        "https://docs.microsoft.com/microsoft-365",
        "https://docs.microsoft.com/office",
        "https://docs.microsoft.com/powershell",
        "https://docs.microsoft.com/sql",
        "https://docs.microsoft.com/surface",
        "https://docs.microsoft.com/system-center",
        "https://docs.microsoft.com/visualstudio",
        "https://docs.microsoft.com/windows",
        "https://docs.microsoft.com/xamarin"
    };
}

一、同步下载

/// 输出显示
private void ReportResult(string result)
{
    Result.Text += result;
}

/// 同步下载按钮被单击
private void SyncDownload_Click(object sender, EventArgs e)
{
    // 同步下载按钮点击后执行的代码
    Result.Text = "";
    // 为了测量程序的执行时间
    var stopwatch = Stopwatch.StartNew();
    // 启动下载的主函数
    DownloadWebsitesSync();
    // 输出函数执行的耗时情况
    Result.Text += $"Elapsed time: {stopwatch.Elapsed}{Environment.NewLine}";
}

/// 遍历所有站点
private void DownloadWebsitesSync()
{
    foreach(var site in Contents.WebSites)
    {
        var result = DownloadWebSiteSync(site);
        ReportResult(result);
    }
}

/// 访问网站获取源代码
private string DownloadWebSiteSync(string url)
{
    var response = httpClient.GetAsync(url).GetAwaiter().GetResult();
    var responsePayloadBytes = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();

    return $"Finish downloding data from {url}. Total bytes returned {responsePayloadBytes.Length}. {Environment.NewLine}";
}

测试

l60lhw4m.png

Finish downloding data from https://www.zhihu.com. Total bytes returned 35556. 
Finish downloding data from https://www.baidu.com. Total bytes returned 9508. 
Finish downloding data from https://www.weibo.com. Total bytes returned 0. 
Finish downloding data from https://www.stackoverflow.com. Total bytes returned 173712. 
Finish downloding data from https://docs.microsoft.com. Total bytes returned 132907. 
Finish downloding data from https://docs.microsoft.com/aspnet/core. Total bytes returned 90052. 
Finish downloding data from https://docs.microsoft.com/azure. Total bytes returned 428349. 
Finish downloding data from https://docs.microsoft.com/azure/devops. Total bytes returned 86931. 
Finish downloding data from https://docs.microsoft.com/dotnet. Total bytes returned 79622. 
Finish downloding data from https://docs.microsoft.com/dynamics365. Total bytes returned 56803. 
Finish downloding data from https://docs.microsoft.com/education. Total bytes returned 39267. 
Finish downloding data from https://docs.microsoft.com/enterprise-mobility-security. Total bytes returned 31372. 
Finish downloding data from https://docs.microsoft.com/gaming. Total bytes returned 61895. 
Finish downloding data from https://docs.microsoft.com/graph. Total bytes returned 45606. 
Finish downloding data from https://docs.microsoft.com/microsoft-365. Total bytes returned 68829. 
Finish downloding data from https://docs.microsoft.com/office. Total bytes returned 68829. 
Finish downloding data from https://docs.microsoft.com/powershell. Total bytes returned 57777. 
Finish downloding data from https://docs.microsoft.com/sql. Total bytes returned 55297. 
Finish downloding data from https://docs.microsoft.com/surface. Total bytes returned 40182. 
Finish downloding data from https://docs.microsoft.com/system-center. Total bytes returned 43407. 
Finish downloding data from https://docs.microsoft.com/visualstudio. Total bytes returned 31543. 
Finish downloding data from https://docs.microsoft.com/windows. Total bytes returned 27177. 
Finish downloding data from https://docs.microsoft.com/xamarin. Total bytes returned 56064. 
Elapsed time: 00:00:50.6908524

缺点:

使用了Foreach循环遍历网站请求数据占用了UI界面刷新的主线程,造成了UI刷新线程的阻塞

在函数执行过程中,界面无法拖动,无法互动,用户体验效果差

耗时50秒,由于是一个一个网址去请求,所以需要等待一个完成或失败后再去进行下一个请求,执行速度较慢!

二、异步下载(一)-异步下载

/// 异步下载按钮被单击
private void AsyncDownload_Click(object sender, EventArgs e)
{
    Result.Text = "";

    var stopwatch = Stopwatch.StartNew();

    DownloadWebsitesAsync();

    Result.Text += $"Elapsed time: {stopwatch.Elapsed}{Environment.NewLine}";
}

/// 使用 async 用于提醒编译器 该方法中使用到了 Await 关键字
/// 对于希望使用Async和Await关键字的方法,返回值必须是Task或者是Task泛型
private async Task DownloadWebsitesAsync()
{
    foreach (var site in Contents.WebSites)
    {
        var result = await Task.Run(() => DownloadWebSiteSync(site));
        ReportResult(result);
    }
}

测试

l60lib5c.png

AsyncDownload_Click中并没有等待DownloadWebSitesAsync执行结束就打印了最后的执行时间

相当于DownloadWebSitesAsync被忽略了。

修改

/// 异步下载按钮被单击
private async void AsyncDownload_Click(object sender, EventArgs e)
{
    Result.Text = "";

    var stopwatch = Stopwatch.StartNew();

    await DownloadWebsitesAsync();

    Result.Text += $"Elapsed time: {stopwatch.Elapsed}{Environment.NewLine}";
}

总结

经过如上编写,发现在测试异步下载时,UI界面线程不会阻塞,可以在下载时与界面进行交互。

通过对比同步和异步两种下载方式,发现两者下载速度并没有太大差别。

于是我们可以发现仅仅使用asyncawait并不能够提升我们处理数据的速度,给我们带来的仅仅是这个UI的响应更加及时。

三、异步下载(二)-并发下载

在上一节分发下载任务时,使用的是foreach进行遍历网址列表,逐个进行下载数据。

虽然Ui Thread并没有被阻塞,但这并没有改变下载数据的策略,数据还是需要从这些网站中一个一个的下载。

这样其实就很好理解,为什么在异步的模型中下载数据的时间总时长并没有被缩短。

如果想缩短下载所有数据的总时间,那么就需要引入: 并行下载 即我们需要更多的线程并发的从这些网站中下载数据。

/// 分发下载任务的主函数
private async Task DownloadWebsitesAsync()
{
    /// 用于存储所有的Task
    List<Task<string>> downloadWebsiteTasks = new List<Task<string>>();

    /// 将原来的下载完成一个再进行下一个,改成快速遍历所有站点,将每一个下载任务存入集合,集合中所有元素并发进行下载。
    foreach (var site in Contents.WebSites)
    {
        downloadWebsiteTasks.Add(Task.Run(() => DownloadWebSiteSync(site)));
    }

    // 当集合所有任务都完成时,统一拿到Result Array<string>返回值
    var results = await Task.WhenAll(downloadWebsiteTasks).Result;

    // 将所有results 全部打印出去。
    foreach(var result in results)
    {
        ReportResult(result);
    }
}

注意:每次程序的第一次完成IO操作时,尤其是这种多线程从远端服务器需要用http下载数据,程序往往会经历一些冷启动的现象,原因是因为我们的 CLR 需要创建相应的Thread Pool来初始化Thread

而初始化每一个Thread也是需要时间的,并且完成这些Http ConnectionTCP Connection的建立也是需要时间的。

因此程序运行的第一次下载耗时不太稳定,会花费额外的时间,如果想要查看稳定的数据下载,需要多次Run一下这个实验的结果。

测试

l60likz1.png

观测到 异步下载+并行下载的时间稳定在2s左右

修改

减少由于初始化很多线程而造成的速度不稳定冷启动时间
/// 访问网站获取源代码 更改为异步操作,命名遵循范式
private async Task<string> DownloadWebSiteAsync(string url)
{
    var response = await httpClient.GetAsync(url); // 直接调用异步方法
    var responsePayloadBytes = await response.Content.ReadAsByteArrayAsync(); // 直接调用异步方法

    return $"Finish downloding data from {url}. Total bytes returned {responsePayloadBytes.Length}. {Environment.NewLine}";
}

private async Task DownloadWebsitesAsync()
{
    List<Task<string>> downloadWebsiteTasks = new List<Task<string>>();

    foreach (var site in Contents.WebSites)
    {
        // 这时候不需要一个额外的Thread来进行等待
        // 所以不再需要Tash.Run,可以直接调用方法。
        downloadWebsiteTasks.Add(DownloadWebSiteAsync());
    }

    var results = await Task.WhenAll(downloadWebsiteTasks).Result;

    foreach(var result in results)
    {
        ReportResult(result);
    }
}

l60liqpi.png

程序运行后的第一次下载:耗时2s

0

评论 (0)

取消