找回密码
 立即注册
首页 业界区 业界 我所知道的.NET异步

我所知道的.NET异步

呈步 2025-5-29 16:07:50
对于异步,相信大家都不十分陌生。准确点来说就是方法执行后立即返回,待到执行完毕会进行通知。就是当一个任务在执行的时候,尤其是需要耗费很长的时间进行处理的任务,如果利用单线程进行操作的话,势必造成界面的阻塞;而利用异步方式,则不会出现这种情况。 区别于同步处理,可以说阻塞的异步其实就相当于同步。
同步方式的实现
先来看一个同步的例子:
假设现在我们需要导入文本文件的内容,然后对文件内容做处理。那么这就需要分为两步来进行,第一步是导入文本内容,我们利用函数A表示;第二部就是处理文本,我们利用函数B来表示。假设现在A不执行完,B不能进行。而且由于文本内容非常大,导入需要十几到几十分钟不等,那么我们得提示用户导入进度,这里就涉及到了界面交互问题。利用同步方式来做,效果如何呢?首先请看运行效果:
1.png

其实上面的图片是我运行了一段时间的程序的截图,但是由于作用在了同步模式下,导致界面阻塞,从而产生极差的用户体验。
代码如下:
2.gif
3.gif
View Code
  1. #region 第一步:加载进入内存<br>        private void ReadIntoMemory()<br>        {<br>            if (String.IsNullOrEmpty(fileName))<br>            {<br>                MessageBox.Show("文件名不能为空!");<br>                return;<br>            }<br><br>            string result;<br>            long mainCount = 0;<br>            using (StreamReader sr = new StreamReader(fileName, Encoding.Default))<br>            {<br>                while ((result = sr.ReadLine()) != null)<br>                {<br>                    mainCount++;<br><br>                    recordList.Add(result); //添加记录到List中存储,以便在下一步进行处理。<br><br>                    double statusResult = (double)mainCount / (double)totalCount;<br><br>                    lblCurrentRecords.Text = mainCount.ToString();<br>                    lblStatus.Text = statusResult.ToString("p");<br>                    pbMain.Value = Int32.Parse((Math.Floor(statusResult)*100).ToString());<br>                }<br>            }<br>        }<br>        #endregion<br><br>        #region 第二步:处理数据<br>        private void ProcessRecords()<br>        {<br>            if (recordList ==null)<br>            {<br>                throw new Exception("数据不存在!");<br>            }<br><br>            if (recordList.Count==0)<br>            {<br>                return;<br>            }<br><br>            int childCount = 0;<br>            int recordCount = recordList.Count;<br><br>            for (int i = 0; i < recordCount; i++)<br>            {<br>                string thisRecord=recordList[i];<br>                if (String.IsNullOrEmpty(thisRecord) || !thisRecord.Contains(","))<br>                {<br>                    return;<br>                }<br><br>                string[] result = thisRecord.Split(',');<br>               <br>                ListViewItem lvi = new ListViewItem(result[0]);<br><br>                for (int j = 1; j < result.Length; j++)<br>                {<br>                    lvi.SubItems.Add(result[j]);<br>                }<br>                listItem.Add(lvi);<br><br>                childCount++;<br>                double percentage = (double)childCount / (double)recordCount;<br>                pbChild.Value = Int32.Parse((Math.Floor(percentage) * 100).ToString());<br>            }<br>        }<br>        #endregion
复制代码
那么我们是如何运行的呢:
  1.         #region 开始进行处理<br>        private void btnLoad_Click(object sender, EventArgs e)<br>        {<br>            GetTotalRecordNum(); //得到总条数<br><br>            ReadIntoMemory();<br>            ProcessRecords();<br>        }<br>        #endregion
复制代码
看到了没,我们是直接顺序运行的。之所以出现上面的情况,最主要就是界面处理和后台处理均糅合在了同一个线程之中,这样当后台进行数据处理的时候,会造成前台UI线程无法更新UI。要解决这种情况,当然是使用异步方式类处理。
那么在.net编程中,有哪几种模式可以实现异步呢?
4种异步方式

  • ThreadPool.QueueUserworkItem实现
  • APM模式(就是BeginXXX和EndXXX成对出现。)
  • EAP模式(就是Event based, 准确说来就是任务在处理中或者处理完成,会抛出事件)
  • Task
上面总共4种方式中,其中在.net 2.0中常用的是(1),(2),(3),而在.net 4.0中支持的是(4),注意(4)在.net 2.0中是不能使用的,因为不存在。
首先来说说ThreadPool.QueueUserWorkItem方式,也是最简单的一种方式。
系统将需要运行的任务放到线程池中,那么线程池中的任务就有机会通过并行的方式进行运行。
其次来说说APM模式
这种模式非常常见,当然也是Jeff Richter极力推荐的一种方式。同时我也是这种模式的粉丝。这种模式的使用非常简单,就是利用Begin***的方式将需要进行异步处理的任务放入,然后通过End***的方式来接受方法的返回值。同时在Begin***和End***任务进行的过程中,如果涉及到界面UI的更新的时候,我们完全可以加入通知的功能。
在Begin***和End***进行处理的时候,传递的是IAsyncResult对象,这种对象在Begin***中会承载一个委托对象,然后在End***中进行还原并得到返回值。
如果你在设计的时候,需要有多个方法用到异步,并且想控制他们的运行顺序,请参考ManualResetEvent 和 AutoResetEvent方法,他们均是通过设置信号量来进行同步的。
下面来看一个例子:
假设现在我们需要导入文本文件的内容,然后对文件内容做处理。那么这就需要分为两步来进行,第一步是导入文本内容,我们利用函数A表示;第二部就是处理文本,我们利用函数B来表示。假设现在A不执行完,B不能进行。而且由于文本内容非常大,导入需要十几到几十分钟不等,那么我们得提示用户导入进度,这里就涉及到了界面交互问题。利用APM模式如何来做呢?首先请看运行效果:
4.jpeg

代码如下:
  1.   #region 典型的APM处理方式,利用Action作为无参无返回值的委托<br>        private void BeginReadIntoMemory()<br>        {<br>            Action action = new Action(ReadIntoMemory);<br>            action.BeginInvoke(new AsyncCallback(EndReadIntoMemory), action);<br>        }<br><br>        private void EndReadIntoMemory(IAsyncResult iar)<br>        {<br>            Action action = (Action)iar.AsyncState;<br>            action.EndInvoke(iar);<br>        }<br><br>        private void BeginProcessRecords()<br>        {<br>            Action action = new Action(ProcessRecords);<br>            action.BeginInvoke(new AsyncCallback(EndProcessRecords), action);<br>        }<br><br>        private void EndProcessRecords(IAsyncResult iar)<br>        {<br>            Action action = (Action)iar.AsyncState;<br>            action.EndInvoke(iar);<br>        }<br>        #endregion
复制代码
我们是如何调用的呢:
  1.         #region 开始进行处理,需要通过ManualResetEvent设置xinhaoilang的方式进行同步<br>        private void btnLoad_Click(object sender, EventArgs e)<br>        {<br>            GetTotalRecordNum(); //得到总条数<br><br>            BeginReadIntoMemory(); //读取数据到内存<br>            BeginProcessRecords(); //处理数据内容<br>        }<br>        #endregion
复制代码
在上面的代码段中,APM模式的处理方式很明显,Begin×××和End×××成对出现,这种方式使用简便,所以很推荐。并且如果涉及到顺序执行的情况,请参加我的前一篇文章:浅谈C#中常见的委托
然后来说说EAP模式
这种模式也很常见,准确来说就是在系统中通过申明委托事件,然后在执行过程中或者执行完毕后抛出事件。最常见的莫过于WebClient类的DownloadStringCompleted事件,这里我们将使用BackgroundWorker来进行讲解,虽然它本身就能够实现异步操作。在这里,我们只是用到了一个从文本中读取大数据量到内存的操作。图示如下:
这里是进行中的操作:
5.jpeg

这里是撤销后的操作:
6.jpeg

那么是如何实现的呢?我们先从BackgroundWorker注册的几个事件说起:
首先是DoWork事件,他的注册方式如下:
  1. bgWorker.DoWork += new DoWorkEventHandler(worker_DoWork);
复制代码
这个主要是用来开启任务的:
  1. private void worker_DoWork(object sender, DoWorkEventArgs e)<br>        {<br>            BackgroundWorker worker = sender as BackgroundWorker;<br><br>            ReadIntoMemory(worker, e); //开始工作<br>        }
复制代码
然后就是ProgressChanged事件,注册方式如下:
  1.          bgWorker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged);
复制代码
从字面上就知道是进行进度报告:
  1. private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)<br>        {<br>            pbMain.Value = e.ProgressPercentage; //利用PrograssBar报告导入进度<br>        }
复制代码
最后就是任务完成报告,注册方式为:
  1. bgWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
复制代码
这里可以进行错误捕获以及任务取消方面的处理:
  1.   private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)<br>        {<br>            if (e.Error != null)<br>            {<br>                MessageBox.Show(e.Error.Message);<br>            }<br>            else if (e.Cancelled)<br>            {<br>                tsInfo.Text = "Data Loading Canceled...";<br>            }<br>            else<br>            {<br>                tsInfo.Text = "Data Loading Completed...";<br>            }<br>        }
复制代码
当然,这个组件在函数运行的过程中,需要向组件传送当前进度的信息,并且在运行过程中,需要检测任务有没有被取消,以达到自动取消任务的功能:
7.gif
8.gif
View Code
  1. #region 第一步:加载数据到内存<br>        private void ReadIntoMemory(BackgroundWorker worker, DoWorkEventArgs e)<br>        {<br><br>            if (String.IsNullOrEmpty(fileName))<br>            {<br>                MessageBox.Show("文件名不能为空!");<br>                return;<br>            }<br><br>            string result;<br>            long mainCount = 0;<br>            using (StreamReader sr = new StreamReader(fileName, Encoding.Default))<br>            {<br>                while ((result = sr.ReadLine()) != null)<br>                {<br>                    mainCount++;<br><br>                    recordList.Add(result); //添加记录到List中存储,以便在下一步进行处理。<br><br>                    double statusResult = (double)mainCount / (double)totalCount;<br>                    syncContext.Send(new SendOrPostCallback((s) =><br>                    {<br>                        if (worker.CancellationPending) //检测到用户取消任务<br>                        {<br>                            e.Cancel = true;  //任务取消<br>                        }<br>                        else<br>                        {<br>                            lblCurrentRecords.Text = mainCount.ToString();<br>                            lblStatus.Text = statusResult.ToString("p");<br>                            int thisPercentange = Int32.Parse((Math.Floor(statusResult * 100)).ToString());<br>                            //pbMain.Value = thisPercentange;<br>                            worker.ReportProgress(thisPercentange); //报告当前的进度<br>                            tsNotify.Text = "| 当前导入";<br>                        }<br>                    }), null);<br><br>                }<br>            }<br>        }<br>        #endregion
复制代码
再说说利用task的实现的方式
 关于Task类,可以说在4.0之前从来没有见过,使用起来非常的简单,也很方便。其实,对于Task类,我也是参考了诸多文章,下面的这句话,引用自另外一篇文章:
Task在并行计算中的作用很凸显,首次构造一个Task对象时,他的状态是Created。以后,当任务启动时,他的状态变成WaitingToRun。Task在一个线程上运行时,他的状态变成Running。任务停止运行,并等待他的任何子任务时,状态变成WaitingForChildrenToComplete。任务完全结束时,它进入以下三个状态之一:RanToCompletion,Canceled或者Faulted。一个Task运行完成时,可通过Task的Result属性来查询任务的结果,一个Task或者Task出错时,可以查询Task的Exception属性来获得任务抛出的未处理的异常,该属性总是返回一个AggregateException对象,他包含所有未处理的异常。
为简化代码,Task提供了几个只读的Boolean属性,IsCanceled,IsFaulted,IsCompleted。注意,当Task处于RanToCompleted,Canceled或者Faulted状态时,IsCompleted返回True。为了判断一个Task是否成功完成,最简单的方法是if(task.Status == TaskStatus.RanToCompletion)。
当然,我们还是以上面的例子来进行编程与讲解。
首先,我们要开启一个Task,那么Task taskOne = new Task(ReadIntoMemory);表示将ReadIntoMemory函数注册成为了任务来运行,然后利用taskOne.Start();来开启任务。那么如何运行第二个任务,并且还要等到第一个运行完成之后呢? 这里我们就需要用到其ContinueWith方法:

Task taskTwo = taskOne.ContinueWith(Action => { ProcessRecords(); });

这样,就行了那么当运行的时候,程序的确会按照顺序来启动任务。图示和APM模式中的图片相同,我就不贴了,下面是代码:
  1.             Task taskOne = new Task(ReadIntoMemory);<br>            taskOne.Start();<br>            Task taskTwo = taskOne.ContinueWith(Action => { ProcessRecords(); });
复制代码
Task泛型方法中的TResult为返回值类型,承载的是一个无参,但是有返回值的任务。所以传入的函数要么是有一个参数带返回值的;要么就是无参数带返回值的,要么就是无参数无返回值的。如果是一个参数,有返回值的话,可以利用下面的方式来进行:
  1. Task<int> taskOne = new Task<int>(a=>ReadIntoMemory((int)a),5);
复制代码
参考资料
同时大家也可以参看我在StackOverflow中的提问,以期起到抛砖引玉的作用:

参考博客:http://hi.baidu.com/jackeyrain/blog/item/828ec3f70bfa8635730eec0a.html

代码下载

点击这里下载

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册