前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C# 学习笔记(18)—— 异步编程

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

作者头像
Karl Du
发布2023-10-20 18:55:52
2540
发布2023-10-20 18:55:52
举报
文章被收录于专栏:Web开发之路Web开发之路

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

什么是异步编程

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

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

同步方式存在的问题

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

代码语言: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();
                }
            }
        }
    }
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021/10/27 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是异步编程
  • 同步方式存在的问题
  • 异步编程模型(APM)
  • 异步编程模型(EAP)
  • 基于任务的异步模式TAP
  • 救星 async / await
    • async 和 await 关系
      • 异步在 Web 和 Winform 中
        • 示例
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档