C# 学习笔记(18)—— 异步编程

2023-10-20 18:55:52 浏览数 (2)

在平时的开发过程中,经常会遇到下载文件、加载资源一类的操作,它们都需要耗费一定的时间才能完成。如果这些程序的代码采用同步方式来实现,将严重影响程序的可操作性,因为在文件下载或资源加载的过程中,我们什么都不能做,只能傻傻地等待,也无法获悉执行进度。为了解决这样地问题,异步编程就孕育而生了

什么是异步编程

异步编程就是把好事地操作放进一个单独地线程中进行处理(该线程需要将执行进度反映到界面上)。由于耗时操作是在另一个线程中被执行的,所以他不会堵塞线程。主线程开启这些单独的线程后,还可以继续执行其他操作(例如窗体绘制等)

异步编程可以提高用户体验,避免在进行耗时操作时让用户看到程序“卡死”的现象

同步方式存在的问题

为了更好地说明异步编程所带来的良好用户体验,我们首先来看采用同步编程会引入哪些问题。文件下载时开发过程中经常遇到的操作,下面以这个操作为例机进行说明。用同步方式实现文件下的代码如下

代码语言: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方法

由于委托类型定义了BeginInvokeEndInvoke方法,所以委托类型都实现了异步编程模型。

在平时的开发过程中,可以使用.Net Framework类中已实现的异步方法来进行异步编程,下面以FileStream类为例来介绍Beginxxx方法和Endxxx方法的使用

代码语言:javascript复制
[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方法的线程上查询IAsyncResultAsyncWaitHandle属性,从而得到WaitHandle对象,接着调用该对象的WaitOne方法来堵塞线程并等待操作完成,最后调用``方法来获得操作结果
  • 在调用Beginxxx方法的线程上循环查询IAsyncResultIsComplete属性,操作完成后再调用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();
        }
    }
}

我们通过SynchronizationContextCurrent属性获得了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 中,微软提出了asyncawait关键字来支持异步编程。这是目前为止最简单的异步编程方式

async 和 await 关系

asyncawait是成对出现的。await只能在async标记的方法里出现。一个方法光有async是没有意义的

代码语言:javascript复制
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应用不同于winformwpf等客户端应用,客户端应用为了保证UI渲染的一致性往往都是采用单线程模式,这个UI线程称为主线程,如果在主线程做耗时操作就会导致程序界面假死,所以客户端开发中使用多线程异步编程非常必要

web应用本身就是多线程模式,服务器会为每个请求分配工作线程

既然async/await不能创建新线程,又不能使提高请求的响应速度,那.NET Web应用中为什么要使用async/await异步编程呢?

在 web 服务器上,.NET Framework 维护用于处理 http://ASP.NET 请求的线程池。当请求到达时,将调度池中的线程以处理该请求。如果以同步方式处理请求,则处理请求的线程将在处理请求时处于繁忙状态,并且该线程无法处理其他请求 在启动时看到大量并发请求的 web 应用中,或具有突发负载(其中并发增长突然增加)时,使 web 服务调用异步会提高应用程序的响应能力。异步请求与同步请求所需的处理时间相同。 如果请求发出需要两秒钟时间才能完成的 web 服务调用,则该请求将需要两秒钟,无论是同步执行还是异步执行。但是,在异步调用期间,线程在等待第一个请求完成时不会被阻止响应其他请求。因此,当有多个并发请求调用长时间运行的操作时,异步请求会阻止请求队列和线程池的增长。

示例

前面下载文件的代码,我们用asyncawait来改写:

代码语言:javascript复制
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();
                }
            }
        }
    }
}

0 人点赞