全面解析C#中的异步编程

当我们处理一些长线的调用时,经常会导致界面停止响应或者IIS线程占用过多等问题,这个时候我们需要更多的是用异步编程来修正这些问题,但是通常都是说起来容易做起来难,诚然异步编程相对于同步编程来说,它是一种完全不同的编程思想,对于习惯了同步编程的开发者来说,在开发过程中难度更大,可控性不强是它的特点。

在.NET Framework5.0种,微软为我们系统了新的语言特性,让我们使用异步编程就像使用同步编程一样相近和简单,本文中将会解释以前版本的Framework中基于回调道德异步编程模型的一些限制以及新型的API如果让我们简单的做到同样的开发任务。

为什么要异步

一直以来,使用远程资源的编程都是一个容易造成困惑的问题,不同于“本地资源”,远程资源的访问总会有很多意外的情况,网络环境的不稳定机器服务端的故障,会造成很多程序员完全不可控的问题,所以这也就要求程序员需要更多的去保护远程资源的调用,管理调用的取消、超市、线程的等待以及处理线程长时间没响应的情况等。而在.NET中我们通常忽略了这些挑战,事实上我们会有多种不用的模式来处理异步编程,比如在处理IO密集型操作或者高延迟的操作时候不组测线程,多数情况我们拥有同步和异步两个方法来做这件事。可是问题在于当前的这些模式非常容易引起混乱和代码错误,或者开发人员会放弃然后使用阻塞的方式去开发。

而在如今的.NET中,提供了非常接近于同步编程的编程体验,不需要开发人员再去处理只会在异步编程中出现的很多情况,异步调用将会是清晰的且不透明的,而且易于和同步的代码进行组合使用。

过去糟糕的体验

最好的理解这种问题的方式是我们最常见的一种情况:用户界面只拥有一个线程所有的工作都运行在这个线程上,客户端程序不能对用户的鼠标时间做出反应,这很可能是因为应用程序正在被一个耗时的操作所阻塞,这可能是因为线程在等待一个网络ID或者在做一个CPU密集型的计算,此时用户界面不能获得运行时间,程序一直处于繁忙的状态,这是一个非常差的用户体验。

很多年来,解决这种问题的方法都是做异步花的调用,不要等待响应,尽快的返回请求,让其他事件可以同时执行,只是当请求有了最终反馈的时候通知应用程序让客户代码可以执行指定的代码。

而问题在于:异步代码完全毁掉了代码流程,回调代理解释了之后如何工作,但是怎么在一个while循环里等待?一个if语句?一个try块或者一个using块?怎么去解释“接下来做什么”?

看下面的一个例子:

public int SumPageSizes(IList<Uri> uris)
        {
            int total = 0;
            foreach (var uri in uris)
            {
                txtStatus.Text = string.Format("Found {0} bytes...", total);
                var data = new WebClient().DownloadData(uri);
                total += data.Length;
            }
            txtStatus.Text = string.Format("Found {0} bytes total", total);
            return total;
        }

这个方法从一个uri列表里下载文件,统计他们的大小并且同时更新状态信息,很明显这个方法不属于UI线程因为它需要花费非常长的时间来完成,这样它会完全的挂起UI,但是我们又希望UI能被持续的更新,怎么做呢?

我们可以创建一个后台编程,让它持续的给UI线程发送数据来让UI来更新自身,这个看起来是很浪费的,因为这个线程把大多时间花在等下和下载上,但是有的时候,这正是我们需要做的。在这个例子中,WebClient提供了一个异步版本的DownloadData方法—DownloadDataAsync,它会立即返回,然后在DownloadDataCompleted后触发一个事件,这允许用户写一个异步版本的方法分割所要做的事,调用立即返回并完成接下来的UI线程上的调用,从而不再阻塞UI线程。下面是第一次尝试:

public void SumpageSizesAsync(IList<Uri> uris)
        {
            SumPageSizesAsyncHelper(uris.GetEnumerator(), 0);
        }

        public void SumPageSizesAsyncHelper(IEnumerator<Uri> enumerator, int total)
        {
            if (enumerator.MoveNext())
            {
                txtStatus.Text = string.Format("Found {0} bytes...", total);
                var client = new WebClient();
                client.DownloadDataCompleted += (sender,e)=>{
                    SumPageSizesAsyncHelper(enumerator, total + e.Result.Length);
                };
                client.DownloadDataAsync(enumerator.Current);
            }
            else
            {
                txtStatus.Text = string.Format("Found {0} bytes total", total);
            }
        }

然后这依然是糟糕的,我们破坏了一个整洁的foreach循环并且手动获得了一个enumerator,每一个调用都创建了一个事件回调。代码用递归取代了循环,这种代码你应该都不敢直视了吧。不要着急,还没有完 。

原始的代码返回了一个总数并且显示它,新的一步版本在统计还没有完成之前返回给调用者。我们怎么样才可以得到一个结果返回给调用者,答案是:调用者必须支持一个回掉,我们可以在统计完成之后调用它。

然而异常怎么办?原始的代码并没有关注异常,它会一直传递给调用者,在异步版本中,我们必须扩展回掉来让异常来传播,在异常发生时,我们不得不明确的让它传播。

最终,这些需要将会进一步让代码混乱:

public void SumpageSizesAsync(IList<Uri> uris,Action<int,Exception> callback)
        {
            SumPageSizesAsyncHelper(uris.GetEnumerator(), 0, callback);
        }

        public void SumPageSizesAsyncHelper(IEnumerator<Uri> enumerator, int total,Action<int,Exception> callback)
        {
            try
            {
                if (enumerator.MoveNext())
                {
                    txtStatus.Text = string.Format("Found {0} bytes...", total);
                    var client = new WebClient();
                    client.DownloadDataCompleted += (sender, e) =>
                    {
                        SumPageSizesAsyncHelper(enumerator, total + e.Result.Length,callback);
                    };
                    client.DownloadDataAsync(enumerator.Current);
                }
                else
                {
                    txtStatus.Text = string.Format("Found {0} bytes total", total);
                    enumerator.Dispose();
                    callback(total, null);
                }

            }
            catch (Exception ex)
            {
                enumerator.Dispose();
                callback(0, ex);
            }

        }

当你再看这些代码的时候,你还能立马清楚的说出这是什么JB玩意吗?

恐怕不能,我们开始只是想和同步方法那样只是用一个异步的调用来替换阻塞的调用,让它包装在一个foreach循环中,想想一下试图去组合更多的异步调用或者有更复杂的控制结构,这不是一个SubPageSizesAsync的规模能解决的。

我们的真正问题在于我们不再可以解释这些方法里的逻辑,我们的代码已经完全无章可循。异步代码中很多的工作让整件事情看起来难以阅读并且似乎充满了BUG。

一个新的方式

如今,我们拥有了一个新的功能来解决上述的问题,异步版本的代码将会如下文所示:

public async Task<int> SumPageSizesAsync(IList<Uri> uris)
        {
            int total = 0;
            foreach (var uri in uris)
            {
                txtStatus.Text = string.Format("Found {0} bytes...", total);
                var data = await new WebClient().DownloadDataTaskAsync(uri);
                total += data.Length;
            }
            txtStatus.Text = string.Format("Found {0} bytes total", total);
            return total;
        }

除了添加的高亮的部分,上文中的代码与同步版本的代码非常相似,代码的流程也从未改变,我们也没有看到任何的回调,但是这并不代表实际上没有回调操作,编译器会搞定这些工作,不再需要您去关心。

异步的方法是用了Task<int>替代了原来返回的Int类型,Task和Task<T>是在如今的framework提供的,用来代表一个正在运行的工作。

异步的方法没有额外的方法,依照惯例为了区别同步版本的方法,我们在方法名后添加Async作为新的方法名。上文中的方法也是异步的,这表示方法体会让编译器区别对待,允许其中的一部分将会变成回调,并且自动的创建Task<int>作为返回类型。

关于这个方法的解释:在方法内部,调用另外一个异步方法DownloadDataTaskAsync,它快速的返回一个Task<byte[]>类型的变量,它会在下载数据完成以后被激活,到如前为止,在数据没有完成之前,我们不想做任何事,所以我们使用await来等待操作的完成。

看起来await关键字阻塞了线程直到task完成下载的数据可用,其实不然,相反它标志了任务的回调,并且立即返回,当这个任务完成之后,它会执行回调。

Tasks

Task和Task<T>类型已经存在于.NET Framework 4.0中,一个Task代表一个进行时的活动,它可能是一个运行在单独线程中的一个CPU密集型的工作或者一个IO操作,手动的创建一个不工作在单独线程的任务也是非常容易的:

static Task ReadFileAsync(string filePath,out byte[] buffer)
        {
            Stream stream = File.Open(filePath, FileMode.Open);
            buffer = new byte[stream.Length];
            var tcs = new TaskCompletionSource<double>();
            stream.BeginRead(buffer, 0, buffer.Length, arr =>
            {
                var length = stream.EndRead(arr);
                tcs.SetResult(stream.Length);
            }, null);
            return tcs.Task;
        }

一旦创建了一个TaskCompletionSource对象,你就可以返回与它关联的Task对象,问相关的工作完成后,客户代码才得到最终的结果,这时Task没有占据自己的线程。

如果实际任务失败,Task从样可以携带异常并且向上传播,如果使用await将触发客户端代码的异常:

static async void ReadAssignedFile()
        {
            byte[] buffer;
            try
            {
                double length = await ReadFileAsync("SomeFileDoNotExisted.txt", out buffer);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        static Task<double> ReadFileAsync(string filePath,out byte[] buffer)
        {
            Stream stream = File.Open(filePath, FileMode.Open);
            buffer = new byte[stream.Length];
            var tcs = new TaskCompletionSource<double>();
            stream.BeginRead(buffer, 0, buffer.Length, arr =>
            {
                try
                {
                    var length = stream.EndRead(arr);
                    tcs.SetResult(stream.Length);
                }
                catch (IOException ex)
                {
                    tcs.SetException(ex);
                }
            }, null);
            return tcs.Task;
        }

基于任务的异步编程模型

上文中解释了异步方法应该是的样子-Task-based asynchronous Pattern(TAP),上文中异步的体现只需要一个调用方法和异步异步方法,后者返回一个Task或者Task<T>。

下文中将介绍一些TAP中的约定,包括怎么处理“取消”和“进行中”,我们将进一步讲解基于任务的编程模型。

Async和await

理解async方法不运行在自己的线程是非常重要的,事实上,编写一个async方法但是没有任何await的话,它就将会是一个不折不扣的同步方法:

static async Task<int> TenToSevenAsync()
        {
            Thread.Sleep(10000);
            return 7;
        }

假如你调用这个方法,将会阻塞线程10秒后返回7,这也许不是你期望的,在VS中也将得到一个警告,因为这可能永远不是想要的结果。

只有一个async方法运行到一个await语句时,它才立即把控制权返回给调用方,然而只有当等待的任务完成之后,它才会真正的返回结果,这意味着你需要确保async方法中的代码不会做过多的任务或者阻塞性能的调用。下面的实例才是你所期望的效果

static async Task<int> TenToSevenAsync()
{
    await Task.Delay(3000);
    return 7;
}

Task.Delay实际上是异步版本的Tread,Sleep,它返回一个Task,这个Task将会在指定的时间内完成。

时间处理程序和无返回值的异步方法

异步方法可以从其他异步方法使用await创建,但是异步在哪里结束?

在客户端程序中,通常的回答是异步方法由事件发起,用户点击一个按钮,一个异步方法被激活,直到它完成,事件本身并不关系方法何时执行完成。这就是通常所说的“发后既忘”

为了适应这种模式,异步方法通常明确的被设计为“发后既忘”-使用void作为返回值替代Task<TResult>类型,这就让方法可以直接作为一个事件处理程序。当一个void saync的方法执行时,没有Task被返回,调用者也无法追踪调用是否完成。

private async void someButton_Click(object sender, RoutedEventArgs e)
{
    someButton.IsEnabled = false;
    await SumPageSizesAsync(GetUrls()));
    someButton.IsEnabled = true;
}

结束语

越写到最后,越不说人话啦》。。。。。

时间: 2024-11-20 03:56:33

全面解析C#中的异步编程的相关文章

Async in C# 5.0(C#中的异步编程Async) 蜗牛翻译之第一章

p { display: block; margin: 3px 0 0 0; } --> 写在前面 在学异步,有位园友推荐了<async in C#5.0>,没找到中文版,恰巧也想提高下英文,用我拙劣的英文翻译一些重要的部分,纯属娱乐,简单分享,保持学习,谨记谦虚. 如果你觉得这件事儿没意义翻译的又差,尽情的踩吧.如果你觉得值得鼓励,感谢留下你的赞,祝各位爱技术的园友在今后每一次应该猛烈突破的时候,不选择知难而退.在每一次应该独立思考的时候,不选择随波逐流,应该全力以赴的时候,不选择尽力

.Net中的异步编程

.Net中的异步编程? .net中实现异步有两种方式:第一种是多线程的方式,第二种是使用异步函数,其实在异步函数中使用的还是多线程的技术. 异步编程中比较关注也比较重要的技术点在于:1.当异步线程在工作完成时如何通知调用线程:2.当异步线程出现异常的时候该如何处理: 3.异步线程工作的进度如何实时的通知调用线程:4.如何在调用线程中取消正在工作的异步线程,并进行回滚操作. 虽然在.net中提供了众多的异步编程模式,但是推荐最好使用Task类,因为Task类使用线程池中的任务线程,又由线程池管理,

C#中的异步编程Async 和 Await

谈到C#中的异步编程,离不开Async和Await关键字 谈到异步编程,首先我们就要明白到底什么是异步编程. 平时我们的编程一般都是同步编程,所谓同步编程的意思,和我们平时说的同时做几件事情完全不同. 在计算机的世界里,同步编程的意思说 按照顺序来执行,或者说是 一个接着一个地有序的来执行, 比如目前我们在代码中有三件任务来执行,那么必须先执行完第1件,再执行第2件,接下来再执行第3件. 在这个过程中,第1件没有完成,你是没法开始做第2件事情的,必须等待. 比如一个人烧开水需要10分钟,5分钟找

promise 的基本概念 和如何解决js中的异步编程问题 对 promis 的 then all ctch 的分析 和 await async 的理解

* promise承诺 * 解决js中异步编程的问题 * * 异步-同步 * 阻塞-无阻塞 * * 同步和异步的区别? 异步;同步 指的是被请求者 解析:被请求者(该事情的处理者)在处理完事情的时候的通知机制. 异步:当事情处理完成后被请求者会发信息通知请求者该事情处理完成.在这期间被请求者可以选择是继续等待命令请求完成还是去做其他事等待被请求者返回. 同步:当事情处理完成后被请求者不会告知请求者,等到请求者发来询问是才会告知 阻塞:非阻塞 指的是请求者 阻塞:针对请求者来说的,委托其他人处理一

Jquery中的异步编程浅析 延期(deferred)的承诺(promise)

引子 相信各位developers对js中的异步概念不会陌生,异步操作后的逻辑由回调函数来执行,回调函数(callback function)顾名思义就是"回头调用的函数",函数体事先已定义好,在未来的某个时候由某个事件触发调用,而这个时机,是程序本身无法控制的. 举几个常见例子: 事件绑定 动画 Ajax 上面的例子简单.典型,易于阅读和理解. 为了引出本文的主题,假设现在有3个ajax异步操作,分别为A.B.C,每个都封装成了函数,并可传入success回调作为参数. 请考虑以下场

.NET中的异步编程——动机和单元测试

背景 自.NET 4.5发布以来已经有很长一段时间了.留在了我们的记忆里,其发布在2012年8月15日.是的,六年前.感觉老了吗?好吧,我不打算让你做出改变,而是提醒你一些.NET发布的亮点.此版本带来的主要功能之一是使用async / await方法进行异步编程.基本上,微软的团队通过保持类似于同步代码的逻辑结构,使编译器完成开发人员过去经常做的工作. 你看,在那个时候,Windows Phone仍然是一件事情,为这些平台开发应用程序有一定的局限性.主要原因是Windows Phone与桌面应

C#中的异步编程:await和async

根据代码示例来学习, 创建一个函数来模拟时间消耗的方法,此处为GetSomeThing函数. 要使用异步编程,需要使用一个async修饰的方法来包装调用GetSomeThing函数,此函数的返回值为Task类型,该类型表示进行并行运算的任务引用.此处示例为ConsumeManyTime函数. 现在就可以直接使用异步方式了,参考TestOne()函数中的代码,其实就是直接调用第二步中的函数ConsumeManyTime(); 总结:异步编程虽然示例三步,但是实际上就是两步的事情,第一步是模拟应用,

在JavaScript中实现异步编程模式的方法

本文总结了”异步模式”编程的4种方法,理解它们可以让你写出结构更合理.性能更出色.维护更方便的Javascript程序. 一.回调函数 这是异步编程最基本的方法. 假定有两个函数f1和f2,后者等待前者的执行结果. f1(); f2(); 如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数. function f1(callback){ setTimeout(function () { // f1的任务代码 callback(); }, 1000); } 执行代码就变成下面这

linux驱动程序中的异步编程

A 前面介绍的等待队列和轮询编程提供了较好的解决设备访问的机制,但是这些机制都 是由应用程序发起的,都需要应用程序主动访问设备.更完美的方式是由驱动程序主 动通知应用程序,也就是说,当驱动程序满足某些条件后,会主动通知应用程序处理 ,这些处理方式有些像面向对象编程的事件,而在linux内核使用的事件是接下来要介 绍的信号. #include<sys/types.h> #include<sys/stat.h> #include<stdio.h> #include<f