在平时的开发过程中,经常会遇到下载文件、加载资源一类的操作,它们都需要耗费一定的时间才能完成。如果这些程序的代码采用同步方式来实现,将严重影响程序的可操作性,因为在文件下载或资源加载的过程中,我们什么都不能做,只能傻傻地等待,也无法获悉执行进度。为了解决这样地问题,异步编程就孕育而生了
什么是异步编程
异步编程就是把好事地操作放进一个单独地线程中进行处理(该线程需要将执行进度反映到界面上)。由于耗时操作是在另一个线程中被执行的,所以他不会堵塞线程。主线程开启这些单独的线程后,还可以继续执行其他操作(例如窗体绘制等)
异步编程可以提高用户体验,避免在进行耗时操作时让用户看到程序“卡死”的现象
同步方式存在的问题
为了更好地说明异步编程所带来的良好用户体验,我们首先来看采用同步编程会引入哪些问题。文件下载时开发过程中经常遇到的操作,下面以这个操作为例机进行说明。用同步方式实现文件下的代码如下
代码语言:javascript复制using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
txbUrl.Text = "https://download.microsoft.com/download/7/0/3/703455ee-a747-4cc8-bd3e-98a615c3aedb/dotNetFx35setup.exe";
}
private void btnDownload_Click(object sender, EventArgs e)
{
rtbState.Text = "下载中....";
if (txbUrl.Text == string.Empty)
{
MessageBox.Show("情先输入下载地址");
return;
}
DownloadFileSync(txbUrl.Text.Trim());
}
public void DownloadFileSync(string url)
{
int BufferSize = 2048;
byte[] BufferRead = new byte[BufferSize];
string savepath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) "dotNetFx 35setup.exe";
FileStream fileStream = null;
HttpWebResponse httpWebResponse = null;
if (File.Exists(savepath))
{
File.Delete(savepath);
}
fileStream = new FileStream(savepath, FileMode.OpenOrCreate);
try
{
HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
if (httpWebRequest != null)
{
httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();
Stream responseStream = httpWebResponse.GetResponseStream();
int readSize = responseStream.Read(BufferRead, 0, BufferSize);
while (readSize > 0)
{
fileStream.Write(BufferRead, 0, readSize);
readSize = responseStream.Read(BufferRead, 0, BufferSize);
}
rtbState.Text = "文件下载完成,文件大小为:" fileStream.SafeFileHandle "下载路径为:" savepath;
}
}
catch (Exception e)
{
rtbState.Text = "下载过程中发生异常,异常信息为:" e.Message;
}
finally
{
if (httpWebResponse != null)
{
httpWebResponse.Close();
}
if (fileStream != null)
{
fileStream.Close();
}
}
}
}
}
在以上代码中,我们首先在窗体构造函数中初始化了文件下载地址,接着在下载按钮单击事件中同步调用了下载文件的方法(即没有单独开启一个线程)。
异步编程模型(APM)
APM是Asynchronous Programming Model
的缩写,即异步编程的意思,它允许程序用更少的线程去执行更多的操作。再.Net Framework中,要分辨某个类是否实现了异步编程模型,主要就是看该类是否实现了类型为IAsyncResult
接口的Beginxxx
方法和Endxxx
方法
由于委托类型定义了BeginInvoke
和EndInvoke
方法,所以委托类型都实现了异步编程模型。
在平时的开发过程中,可以使用.Net Framework
类中已实现的异步方法来进行异步编程,下面以FileStream
类为例来介绍Beginxxx
方法和Endxxx
方法的使用
[SecuritySafeCritical]
public override IAsyncResult BeginRead(byte[] array, int offset, int numBytes, AsyncCallback userCallback, object stateObject);
[SecuritySafeCritical]
public override IAsyncResult BeginWrite(byte[] array, int offset, int numBytes, AsyncCallback userCallback, object stateObject);
我们看到,异步方法前面三个参数和同步方法一致,后两个参数则是同步方法不具备的,userCallback
表示异步操作完成后需要的回调,该方法必须匹配AsyncCallBack
委托类型;stateObject
则代表传递给回调方法的对象,在回调方法中,可以通过查询IAsyncResult
接口的AsyncState
属性来读取该对象
该异步方法之所以不会堵塞UI线程,是因为它在被调用后,会立即把控制权交还给调用线程。
APM给出了四种方式来访问异步操作所得到地结果
- 在调用
Beginxxx
方法的线程上调用Endxxx
方法来得到异步操作的结果。然而这种方式会阻塞调用线程,使其一致挂起,直至完成 - 在调用
Beginxxx
方法的线程上查询IAsyncResult
的AsyncWaitHandle
属性,从而得到WaitHandle
对象,接着调用该对象的WaitOne
方法来堵塞线程并等待操作完成,最后调用``方法来获得操作结果 - 在调用
Beginxxx
方法的线程上循环查询IAsyncResult
的IsComplete
属性,操作完成后再调用Endxxx
方法来返回结果 - 使用
AsyncCallback
委托来指定操作完成时要调用的方法,在回调方法中调用Endxxx
方法来获得异步操作返回的结果
在上面的四种方式中,前三种都会堵塞线程。因为UI线程在调用Beginxxx方法进行异步操作后,会立即返回并继续执行。此时,已经有另一个线程在执行异步操作(如文件下载)。当UI线程执行到Endxxx方法时,该方法会堵塞UI线程,直到异步操作完成后为止。所以,前三种方式虽然采用了异步编程模型,但结果却与同步方式是一样的。
而最后一种方式由于是在回调方法中调用的Endxxx,而回调方法又是在另一个线程中被执行的,此时堵塞的只是执行异步任务的线程,完全不会堵塞UI线程,因此完美地解决了界面的“假死”情况
下面演示一下第一种方式代码:
代码语言:javascript复制using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
txbUrl.Text = "https://download.microsoft.com/download/7/0/3/703455ee-a747-4cc8-bd3e-98a615c3aedb/dotNetFx35setup.exe";
}
private void btnDownload_Click(object sender, EventArgs e)
{
rtbState.Text = "下载中....";
if (txbUrl.Text == string.Empty)
{
MessageBox.Show("情先输入下载地址");
return;
}
DownloadFileAsync(txbUrl.Text.Trim());
}
public void DownloadFileAsync(string url)
{
int BufferSize = 2048;
byte[] BufferRead = new byte[BufferSize];
string savepath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) "dotNetFx 35setup.exe";
FileStream fileStream = null;
HttpWebResponse httpWebResponse = null;
if (File.Exists(savepath))
{
File.Delete(savepath);
}
fileStream = new FileStream(savepath, FileMode.OpenOrCreate);
try
{
HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
if (httpWebRequest != null)
{
IAsyncResult result = httpWebRequest.BeginGetResponse(null, null);
httpWebResponse = (HttpWebResponse)httpWebRequest.EndGetResponse(result);
Stream responseStream = httpWebResponse.GetResponseStream();
int readSize = responseStream.Read(BufferRead, 0, BufferSize);
while (readSize > 0)
{
fileStream.Write(BufferRead, 0, readSize);
readSize = responseStream.Read(BufferRead, 0, BufferSize);
}
rtbState.Text = "文件下载完成,文件大小为:" fileStream.SafeFileHandle "下载路径为:" savepath;
}
}
catch (Exception e)
{
rtbState.Text = "下载过程中发生异常,异常信息为:" e.Message;
}
finally
{
if (httpWebResponse != null)
{
httpWebResponse.Close();
}
if (fileStream != null)
{
fileStream.Close();
}
}
}
public void DownloadFileSync(string url)
{
int BufferSize = 2048;
byte[] BufferRead = new byte[BufferSize];
string savepath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) "dotNetFx 35setup.exe";
FileStream fileStream = null;
HttpWebResponse httpWebResponse = null;
if (File.Exists(savepath))
{
File.Delete(savepath);
}
fileStream = new FileStream(savepath, FileMode.OpenOrCreate);
try
{
HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
if (httpWebRequest != null)
{
httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();
Stream responseStream = httpWebResponse.GetResponseStream();
int readSize = responseStream.Read(BufferRead, 0, BufferSize);
while (readSize > 0)
{
fileStream.Write(BufferRead, 0, readSize);
readSize = responseStream.Read(BufferRead, 0, BufferSize);
}
rtbState.Text = "文件下载完成,文件大小为:" fileStream.SafeFileHandle "下载路径为:" savepath;
}
}
catch (Exception e)
{
rtbState.Text = "下载过程中发生异常,异常信息为:" e.Message;
}
finally
{
if (httpWebResponse != null)
{
httpWebResponse.Close();
}
if (fileStream != null)
{
fileStream.Close();
}
}
}
}
}
在以上代码中,DownloadFileAsync
方法通过调用BeginGetResponse
方法来异步地请求资源,执行完该方法后立即返回到UI线程中。UI线程继续执行代码,遇到EndGetReponse
方法,此方法会堵塞UI线程,使得程序效果与同步实现地效果一样
下面介绍第四种方式:
代码语言:javascript复制using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.Runtime.Remoting.Messaging;
namespace WinFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
txbUrl.Text = "https://download.microsoft.com/download/7/0/3/703455ee-a747-4cc8-bd3e-98a615c3aedb/dotNetFx35setup.exe";
}
private delegate string AsyncMethodCaller(string fileurl);
SynchronizationContext sc;
private void btnDownload_Click(object sender, EventArgs e)
{
rtbState.Text = "下载中....";
if (txbUrl.Text == string.Empty)
{
MessageBox.Show("情先输入下载地址");
return;
}
sc = SynchronizationContext.Current;
AsyncMethodCaller methodCaller = new AsyncMethodCaller(DownloadFileAsync);
methodCaller.BeginInvoke(txtUrl.Text.Trim(), GetResult, null);
}
public void DownloadFileAsync(string url)
{
int BufferSize = 2048;
byte[] BufferRead = new byte[BufferSize];
string savepath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) "dotNetFx 35setup.exe";
FileStream fileStream = null;
HttpWebResponse httpWebResponse = null;
if (File.Exists(savepath))
{
File.Delete(savepath);
}
fileStream = new FileStream(savepath, FileMode.OpenOrCreate);
try
{
HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
if (httpWebRequest != null)
{
httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(null, null);
Stream responseStream = httpWebResponse.GetResponseStream();
int readSize = responseStream.Read(BufferRead, 0, BufferSize);
while (readSize > 0)
{
fileStream.Write(BufferRead, 0, readSize);
readSize = responseStream.Read(BufferRead, 0, BufferSize);
}
rtbState.Text = "文件下载完成,文件大小为:" fileStream.SafeFileHandle "下载路径为:" savepath;
}
}
catch (Exception e)
{
rtbState.Text = "下载过程中发生异常,异常信息为:" e.Message;
}
finally
{
if (httpWebResponse != null)
{
httpWebResponse.Close();
}
if (fileStream != null)
{
fileStream.Close();
}
}
}
private void GetResult(IAsyncResult result)
{
AsyncMethodCaller caller = (AsyncMethodCaller)((AsyncResult)result).AsyncDelegate;
string returnString = caller.EndInvoke(result);
sc.Post(ShowState, returnString);
}
private void ShowState(object result)
{
rtbState.Text = result.ToString();
btnDownload.Click();
}
}
}
我们通过SynchronizationContext
的Current
属性获得了UI线程的同步上下文对象。处于安全考虑,.Net
规定控件只能被创建它的线程访问,而此时下载文件的操作正在另一个线程中执行,故不能在该线程中访问UI线程的控件
所以,此时要显示下载完成的状态信息,必须要通过SynchronizationContext
对象的Post
方法,把显示状态信息的代码推送UI线程去执行。如果在非UI线程访问控件,则会出现“不能跨线程访问控件”的异常
最后,通过调用委托对象的BeginInvoke
方法来进行异步的文件下载操作。下载完成时,将回调GetResult
方法来获得操作结果
异步编程模型(EAP)
略...
基于任务的异步模式TAP
略...
救星 async / await
虽然,.Net 1.0、.Net 2.0 和 .Net 4.0 都对异步编程做了很好的支持,微软也逐渐地使异步编程变得简单,但是微软觉得还不够,它希望使异步编程开发过程变得更为简单,所以在 .Net 4.5 中,微软提出了async
和await
关键字来支持异步编程。这是目前为止最简单的异步编程方式
async 和 await 关系
async
和await
是成对出现的。await
只能在async
标记的方法里出现。一个方法光有async
是没有意义的
private async Task DoSomething()
{
await Task.Delay(TimeSpan.FormSeconds(10));
}
private async Task<string> GetSomething()
{
var result = await new HttpClient().SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://localhost:5000/auth/test"));
return result;
}
异步在 Web 和 Winform 中
我们都知道web
应用不同于winform
、wpf
等客户端应用,客户端应用为了保证UI渲染的一致性往往都是采用单线程模式,这个UI线程称为主线程,如果在主线程做耗时操作就会导致程序界面假死,所以客户端开发中使用多线程异步编程非常必要
可web
应用本身就是多线程模式,服务器会为每个请求分配工作线程
既然async
/await
不能创建新线程,又不能使提高请求的响应速度,那.NET Web
应用中为什么要使用async
/await
异步编程呢?
在 web 服务器上,.NET Framework 维护用于处理 http://ASP.NET 请求的线程池。当请求到达时,将调度池中的线程以处理该请求。如果以同步方式处理请求,则处理请求的线程将在处理请求时处于繁忙状态,并且该线程无法处理其他请求 在启动时看到大量并发请求的 web 应用中,或具有突发负载(其中并发增长突然增加)时,使 web 服务调用异步会提高应用程序的响应能力。异步请求与同步请求所需的处理时间相同。 如果请求发出需要两秒钟时间才能完成的 web 服务调用,则该请求将需要两秒钟,无论是同步执行还是异步执行。但是,在异步调用期间,线程在等待第一个请求完成时不会被阻止响应其他请求。因此,当有多个并发请求调用长时间运行的操作时,异步请求会阻止请求队列和线程池的增长。
示例
前面下载文件的代码,我们用async
和await
来改写:
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
txbUrl.Text = "https://download.microsoft.com/download/7/0/3/703455ee-a747-4cc8-bd3e-98a615c3aedb/dotNetFx35setup.exe";
}
private async void btnDownload_Click(object sender, EventArgs e)
{
rtbState.Text = "下载中....";
if (txbUrl.Text == string.Empty)
{
MessageBox.Show("情先输入下载地址");
return;
}
await DownloadFileAsync(txbUrl.Text.Trim());
}
public async Task DownloadFileAsync(string url)
{
int BufferSize = 2048;
byte[] BufferRead = new byte[BufferSize];
string savepath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) "dotNetFx 35setup.exe";
FileStream fileStream = null;
HttpWebResponse httpWebResponse = null;
if (File.Exists(savepath))
{
File.Delete(savepath);
}
fileStream = new FileStream(savepath, FileMode.OpenOrCreate);
try
{
HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
if (httpWebRequest != null)
{
httpWebResponse = (HttpWebResponse)await httpWebRequest.GetResponseAsync();
Stream responseStream = httpWebResponse.GetResponseStream();
int readSize = responseStream.Read(BufferRead, 0, BufferSize);
while (readSize > 0)
{
fileStream.Write(BufferRead, 0, readSize);
readSize = responseStream.Read(BufferRead, 0, BufferSize);
}
rtbState.Text = "文件下载完成,文件大小为:" fileStream.SafeFileHandle "下载路径为:" savepath;
}
}
catch (Exception e)
{
rtbState.Text = "下载过程中发生异常,异常信息为:" e.Message;
}
finally
{
if (httpWebResponse != null)
{
httpWebResponse.Close();
}
if (fileStream != null)
{
fileStream.Close();
}
}
}
}
}