C#异步编程综述

by shinichi_wtn 2012-12-28 14:20

异步编程简介

异步操作通常用于执行完成时间可能较长的任务,如打开大文件、连接远程计算机或查询数据库。异步操作在主应用程序线程以外的线程中执行,所以当应用程序调用方法异步执行某个操作时,应用程序可在异步方法执行其任务时继续执行。

我们在写界面时会大量涉及异步操作,一般用时可能操作20ms的操作都应该设计为异步,以保证最佳的用户体验。

C#中实现异步的传统方法

其实并不需要了解C#的多线程编程,就可以很好的编写异步应用,.NET Framework中很多方法已经封装为异步(尤其是Silverlight SDK与Windows Phone SDK),方法里如果有BeginXXX/EndXXX都是异步方法,比如HttpWebRequest,下面的代码给出了异步Http请求的实现方法

private static void BeginGetResponse()
{
    WriteThreadID("BeginGetResponse");
    HttpWebRequest request = HttpWebRequest.CreateHttp("http://www.pku.edu.cn");
    request.BeginGetResponse(EndGetResponse, request);
}

private static void EndGetResponse(IAsyncResult a)
{
    WriteThreadID("EndGetResponse");
    HttpWebRequest request = a.AsyncState as HttpWebRequest;
    try
    {
        HttpWebResponse response = request.EndGetResponse(a) as HttpWebResponse;
        if (response.StatusCode == HttpStatusCode.OK)
        {
            Console.WriteLine("Request succeed");
        }
        else
        {
            Console.WriteLine("Response error: status code = " + response.StatusCode);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Request failed: " + ex.Message);
    }
}
//输出结果
BeginGetResponse running in thread 9
EndGetResponse running in thread 14
Request succeed

为了知道每个方法到底在哪个线程中运行,这里写了一个简单的函数WriteThreadID用来输出当前执行线程的ID,从而验证异步方法执行的线程和主线程是不一样的

private static void WriteThreadID(string methodName)
{
    Console.WriteLine(methodName + " running in thread " + System.Threading.Thread.CurrentThread.ManagedThreadId);
}

其实我们也可以改用匿名方法,这样就可以在一个方法体里面完成所有逻辑

private static void BeginGetResponseAnonymous()
{
    WriteThreadID("BeginGetResponseAnonymous");
    HttpWebRequest request = HttpWebRequest.CreateHttp("http://www.pku.edu.cn");
    request.BeginGetResponse(a =>
    {
        try
        {
            WriteThreadID("AnonymousMethod");
            HttpWebResponse response = request.EndGetResponse(a) as HttpWebResponse;
            if (response.StatusCode == HttpStatusCode.OK)
            {
                Console.WriteLine("Request succeed");
            }
            else
            {
                Console.WriteLine("Response error: status code = " + response.StatusCode);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("Request failed: " + ex.Message);
        }
    }, null);
}

如果.NET Framework里某些操作并非自带异步,或者我们要异步执行我们自己的方法,这个时候就要自己编写异步逻辑了。当然.NET Framework里提供了很多方法帮助我们完成异步操作,这里主要介绍两种方法:委托DelegateBackgroundWorker,一般应用使用BackgroundWorker足矣。当然,自己创建线程Thread也可以实现类似的功能,甚至直接用.NET的线程池ThreadPool(值得一提的是Delegate和BackgroundWorker的实现底层都用了这个),不过本文不涉及它们,感兴趣的可以参看C#多线程编程。

这里用一个简单的公共例子,分别用Delegate、Background和Thread来完成异步操作。假设我们有一个GetString的方法如下,其实就是模拟一个较为耗时的方法,然后返回"Hello World"。

private static string GetString()
{
    WriteThreadID("GetString");
    System.Threading.Thread.Sleep(1000);
    return "Hello World";
}

委托Delegate

.NET中的Delegate类型是一个类型安全的、面向对象的函数指针,委托具有异步的天生性,因为每个被定义的委托都将被编译器编译为一个继承自System.MulticastDelegate的类,类里有熟悉的BeginInvoke和EndInvoke方法,这个就是异步执行委托的基础,使用就和前面例子中的HttpWebRequest完全一样。

由于GetString是一个没有参数只有返回值的方法,所以我们直接用系统自有的委托类型Func<string>,基于委托的异步调用实现如下

private static void BeginGetStringAsyncDelegate()
{
    WriteThreadID("BeginGetStringAsyncDelegate");
    Func<string> method = GetString;
    IAsyncResult iar = method.BeginInvoke(EndGetStringAsyncDelegate, method);
}

private static void EndGetStringAsyncDelegate(IAsyncResult iar)
{
    WriteThreadID("EndGetStringAsyncDelegate");
    Func<string> method = iar.AsyncState as Func<string>;
    string result = method.EndInvoke(iar);
    Console.WriteLine(result);
}
//输出结果
BeginGetStringAsyncDelegate running in thread 9
GetString running in thread 10
EndGetStringAsyncDelegate running in thread 10
Hello World

BackgroundWorker类

BackgroundWorker是.NET封装很好的一个专用于后台(异步)方法执行的类,在System.ComponentModel命名空间下;它不仅封装了异步操作的逻辑,同时还提供了进度控制、取消异步操作等等逻辑(详细的使用说明可以参见msdn,这里只涉及最基本的功能)。使用BackgroundWorker来完成之前的例子如下

private static void BeginGetStringAsyncBackgroundWorker()
{
    WriteThreadID("BeginGetStringAsyncBackgroundWorker");
    BackgroundWorker worker = new BackgroundWorker();
    worker.DoWork += worker_DoWork;
    worker.RunWorkerCompleted += worker_RunWorkerCompleted;
    worker.RunWorkerAsync();
}

private static void worker_DoWork(object sender, DoWorkEventArgs e)
{
    e.Result = GetString();
}

private static void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    WriteThreadID("worker_RunWorkerCompleted");
    string result = e.Result as string;
    Console.WriteLine(result);
}
//输出结果
BeginGetStringAsyncBackgroundWorker running in thread 10
GetString running in thread 6
worker_RunWorkerCompleted running in thread 11
Hello World

可以看到,DoWork事件里调用我们的异步方法,RunWorkerCompleted事件为异步方法执行完成后的回调函数,通过e.Result传递结果值。

传统异步编程的小结

无论是使用Delegate还是BackgroundWorker,创建异步操作总是需要至少两个方法(一个Begin,一个End),虽然我们可以用匿名方法将所有代码放到一个方法体中,但是仍然无法避免代码被打乱分散的事实,因为它与传统的同步编程思维不一致。.NET 4.5引入了全新的异步编程模型async/await,正是为了简化我们异步编程

.NET Framework 4.5异步编程async/await简介

MSDN(http://msdn.microsoft.com/en-us/library/vstudio/hh191443.aspx)上给出了相关背景

You can avoid performance bottlenecks and enhance the overall responsiveness of your application by using asynchronous programming. However, traditional techniques for writing asynchronous applications can be complicated, making them difficult to write, debug, and maintain.

Visual Studio 2012 introduces a simplified approach, async programming, that leverages asynchronous support in the .NET Framework 4.5 and the Windows Runtime. The compiler does the difficult work that the developer used to do, and your application retains a logical structure that resembles synchronous code. As a result, you get all the advantages of asynchronous programming with a fraction of the effort.

async/await是在.NET Framework 4.5开始引入的新关键词,它基于.NET 4.0开始引入的Task类库(System.Threading.Tasks)。

我们在异步执行的方法声明中加入async关键词,告诉编译器这个方法里包含异步逻辑,然后我们就可以在方法体里面使用await关键词去等待某个Task的执行,在Task执行结束前,await后面的代码不会执行,而是跳回调用方方便其后续方法继续执行,直到异步方法执行完成后才接着执行方法体里后面的代码

我们用async/await来实现GetString的例子如下,两行代码就搞定了所有的异步操作

private static async void GetStringAsync()
{
    WriteThreadID("GetStringAsync Begin Part");
    string result = await Task.Run<string>(() => GetString());
    WriteThreadID("GetStringAsync End Part");
    Console.WriteLine(result);
}
//输出结果
GetStringAsync Begin Part running in thread 9
GetString running in thread 10
GetStringAsync End Part running in thread 10
Hello World

接着我们作为调用方写一个方法DoMyWork调用一个异步方法,并且调用异步方法后面我们再执行一个方法,代码及输出结果如下

private static void DoMyWork()
{
    GetStringAsyncPure();
    Console.WriteLine("doing other works...");
}

private static async void GetStringAsyncPure()
{
    string result = await Task.Run<string>(() => GetStringPure());
    Console.WriteLine(result);
}

private static string GetStringPure()
{
    System.Threading.Thread.Sleep(1000);
    return "Hello World";
}
//输出结果
doing other works...
Hello World

从输出结果可以看到,"doing other works..."首先被输出,即异步方法没有阻碍调用方后续方法的执行,这也正是异步的核心思想。

MSDN那篇文章详细讲解了async/await的实现和执行方式,感兴趣的童鞋可以阅读。得益于新的编译器和Task类库,让我们实现异步于无形中。我们可以在一个async方法里调用多个await,调用多个异步方法(在传统方法中我们要写更多的回调),大大节省了代码量,也极大地改进了代码的可读性。

.NET Framework 4.5有很多类自带了Async为后缀的方法,主要集中在IO和网络方便的类库,比如StreamReader就多了4个异步方法ReadAsyncReadBlockAsyncReadLineAsyncReadToEndAsync,我们在读取较大文件的时候可以直接调用。

.NET Framework 4.0/Silverlight 5.0使用async/await

由于async/await是.NET 4.5引入的新特性,那么在.NET 4.0或者Silverlight 5.0下是否也可以使用如此杀手级的特性呢?答案是肯定的,因为整套异步框架基于Task Library和编译器,所以我们在VS2012下可以通过Nuget安装Async Targeting Pack这个类库,它可以在.NET 4.0/Silverlight 5.0下使用async/await带来的全新异步编程体验。

总结

本文详细介绍了C#异步编程的实现方法,MSDN上已经说了,async/await在任何场景下都是实现异步的最好方案,完全可以代替BackgroundWorker。所以,如果我们工作在.NET 4.0/.NET 4.5/Silverlight 5.0下,尽可能使用async来做异步,带来的代码节省性和可维护性的优势是巨大的。

The async-based approach to asynchronous programming is preferable to existing approaches in almost every case

本文演示的源代码NETAsync.zip可以在这里下载:http://sdrv.ms/Ttpqm2,需要Visual Studio 2012才能运行。

(仅用于Gavatar)

  Country flag

biuquote
  • Comment
  • Preview
Loading

About

shinichi_wtnI'm Shinichi_wtn

Software Engineering Manager at Microsoft

[More...]


Month List