本文参考: bilibili视频
讲的非常棒!
需求引入 - 下载数据
从不同的网站中下载很大的数据量,并且将数据结果呈现在UI客户端的一个经典的实际场景
界面
代码
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}";
}
测试
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);
}
}
测试
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界面线程不会阻塞,可以在下载时与界面进行交互。
通过对比同步和异步两种下载方式,发现两者下载速度并没有太大差别。
于是我们可以发现仅仅使用async
和await
并不能够提升我们处理数据的速度,给我们带来的仅仅是这个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 Connection
的TCP Connection
的建立也是需要时间的。
因此程序运行的第一次下载耗时不太稳定,会花费额外的时间,如果想要查看稳定的数据下载,需要多次Run一下这个实验的结果。
测试
观测到 异步下载+并行下载的时间稳定在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);
}
}
程序运行后的第一次下载:耗时2s
评论 (0)