.net中async/await异步编程 - 小众知识

.net中async/await异步编程

2013年01月27日 14:18:05 苏内容
  标签: await
阅读:7328

C#5.0中引入了构建异步方法的新特性---async/await。本文根据自己的理解讲述异步方法三种返回方式和取消异步操作的示例。


在此之前先说说异步方法的语法特点:


方法名称一般是Async 结尾。

可以包含一个或者多个await表达式。

异步方法的参数不能使用ref和out参数。

方法头包含async关键字,并且在返回类型之前。

除了方法之外, Lambda 表达式和匿名函数也可以作为异步对象。

先来看看同步程序的运行示例,


DownLoadTest类的代码如下:


        Stopwatch watch = new Stopwatch();

        public DownLoadTest()

        {

            watch.Start();

        } 

        public void DoRun()

        {

            Debug.WriteLine(string.Format("同步程序开始运行:{0,4:N0}ms", watch.Elapsed.TotalMilliseconds));

            string t1 = DownLoadString("https://stackoverflow.com/");

            string t2 = DownLoadString("https://github.com/");

            Debug.WriteLine(string.Format("同步程序运行结束:{0,4:N0}ms", watch.Elapsed.TotalMilliseconds));

        }

        public string DownLoadString(string url)

        {

            Debug.WriteLine(string.Format("下载{0}开始运行 :{1,4:N0}ms", url, watch.Elapsed.TotalMilliseconds));

            WebClient wc = new WebClient();

            string str = wc.DownloadString(url);

            Debug.WriteLine(string.Format("下载{0}运行结束 :{1,4:N0}ms", url, watch.Elapsed.TotalMilliseconds));

            return str;

        }

在控制台运行的代码如下:


        static void Main(string[] args)

        {

            DownLoadTest dwtest = new DownLoadTest();

            dwtest.DoRun();

            Debug.WriteLine(string.Format("主线程运行结束"));

            Console.ReadKey();

        }

运行的结果在调试输出窗口显示如下:

同步程序开始运行:   0ms

下载https://stackoverflow.com/开始运行 :  26ms

下载https://stackoverflow.com/运行结束 :1,860ms

下载https://github.com/开始运行 :1,861ms

下载https://github.com/运行结束 :3,579ms

同步程序运行结束:3,580ms

主线程运行结束

可以看到程序完全的按照代码顺序执行

异步方法示例代码说明:


我们在DownLoadTest添加一个异步的方法,返回Task<string>,代码如下:


        public async Task<string> DownLoadStringAsync(string url)

        {

            string str = await Task.Run(() => DownLoadString(url));

            return str;

        }

这个代码和下面的代码使用DownloadStringTaskAsync其实作用相同:


        public async Task<string> DownLoadStringAsync(string url)

        {

            WebClient wc = new WebClient();

            string str = await wc.DownloadStringTaskAsync(url);

            return str;

        }

1、返回void的情况:


我们在添加返回void的如下代码:


        public async void DoRunNoResultAsync()

        {

            Debug.WriteLine(string.Format("异步程序开始运行:{0,4:N0}ms", watch.Elapsed.TotalMilliseconds));

            await DownLoadStringAsync("https://stackoverflow.com/");

            await DownLoadStringAsync("https://github.com/");

            Debug.WriteLine(string.Format("异步程序运行结束:{0,4:N0}ms", watch.Elapsed.TotalMilliseconds));

        }

在main函数中执行dwtest.DoRunNoResultAsync();得到的结果如下:


异步程序开始运行:   1ms

下载https://stackoverflow.com/开始运行 :  54ms

主线程运行结束

下载https://stackoverflow.com/运行结束 :1,806ms

下载https://github.com/开始运行 :1,807ms

下载https://github.com/运行结束 :3,455ms

异步程序运行结束:3,456ms

去掉DownLoadStringAsync("https://stackoverflow.com/")前面的await,得到如下的结果:


异步程序开始运行:   1ms

下载https://stackoverflow.com/开始运行 :  55ms

主线程运行结束

下载https://github.com/开始运行 :  59ms

下载https://github.com/运行结束 :1,864ms

异步程序运行结束:1,866ms

下载https://stackoverflow.com/运行结束 :1,946ms

可以看到在async方法内,await控制着指明需要异步执行的任务,去掉await后,方法DownLoadStringAsync会显示一个提示信息“由于此调用不会等待,因此在此调用完成之前将会继续执行当前方法,请考虑将await运算符应用于调用结果”。


如果我们去掉另外一个await修饰符,那么async方法会提示一个信息“此异步方法缺少await运算符,将以同步方式运行。请考虑使用await运算符等待非阻止的API调用,或者使用await Task.Run(...)在后台线程上执行占用大量CPU的工作”。执行修改过后的代码如下所示:


异步程序开始运行:   1ms

异步程序运行结束:  51ms

下载https://stackoverflow.com/开始运行 :  54ms

主线程运行结束

下载https://github.com/开始运行 :  57ms

下载https://github.com/运行结束 :1,823ms

下载https://stackoverflow.com/运行结束 :1,841ms

这里因为DownLoadStringAsync本身是异步方法,DoRunNoResultAsync中虽然同步运行,所以显示效果也是异步执行的方式。

2、有返回值的异步方法:


Task<T>为返回值的异步方法,T为返回的类型。如果直接写这样写public async string DoRunNoResultAsync() 这种是不允许的,需要改为public async Task<string> DoRunNoResultAsync()。我们添加一个异步方法


public async Task<string> DoRunStringAsync(string url)

{

    Debug.WriteLine(string.Format("异步程序获取{0}开始运行:{1,4:N0}ms", url, watch.Elapsed.TotalMilliseconds));

    var task = await DownLoadStringAsync(url);

    Debug.WriteLine(string.Format("异步程序获取{0}运行结束:{1,4:N0}ms", url, watch.Elapsed.TotalMilliseconds));

    return task;

}

var task1 = dwtest.DoRunStringAsync("https://stackoverflow.com/");

var task2 = dwtest.DoRunStringAsync("https://github.com/");

Debug.WriteLine("task.Result等待结果打印");

Console.WriteLine(task1.Result);

Console.WriteLine(task2.Result);

Debug.WriteLine(string.Format("主线程运行结束"));

Console.ReadKey();

结果如下:


异步程序获取https://stackoverflow.com/开始运行:   1ms

异步程序获取https://github.com/开始运行:  51ms

task.Result等待结果打印

下载https://stackoverflow.com/开始运行 :  53ms

下载https://github.com/开始运行 :  53ms

下载https://github.com/运行结束 :1,945ms

异步程序获取https://github.com/运行结束:1,946ms

下载https://stackoverflow.com/运行结束 :9,454ms

异步程序获取https://stackoverflow.com/运行结束:9,455ms

主线程运行结束

并会在控制台打印出获取的html代码。Task.Result是获取异步方法返回的T的值,并且程序会等待异步方法得到返回值后再去执行下面的语句。


3、返回Task


异步方法还可以返回Task,代码如下:


public async Task DoRunTaskAsync(string url)

{

    Debug.WriteLine(string.Format("下载{0}开始运行 :{1,4:N0}ms", url, watch.Elapsed.TotalMilliseconds));

    WebClient wc = new WebClient();

    await wc.DownloadStringTaskAsync(url);

    Debug.WriteLine(string.Format("下载{0}运行结束 :{1,4:N0}ms", url, watch.Elapsed.TotalMilliseconds));

}

var task1 = dwtest.DoRunTaskAsync("https://stackoverflow.com/");

var task2 = dwtest.DoRunTaskAsync("https://github.com/");

var task3 = dwtest.DoRunTaskAsync("https://www.google.com//");

Debug.WriteLine("task.Result等待结果打印");

Thread.Sleep(2000);

Console.WriteLine(task1.Status);

Console.WriteLine(task2.Status);

Console.WriteLine(task3.Status);

Debug.WriteLine(string.Format("主线程运行结束"));

执行的结果如下:


下载https://stackoverflow.com/开始运行 :   1ms

下载https://github.com/开始运行 : 477ms

下载https://www.google.com//开始运行 :1,034ms

task.Result等待结果打印

下载https://github.com/运行结束 :6,330ms

下载https://stackoverflow.com/运行结束 :8,048ms

主线程运行结束

控制台显示的结果如下:




控制台打印的是获取此任务的状态,RanToCompletion代表已成功完成执行的任务,WaitingForActivation代表正在等待该任务的执行结果。


4、取消一个异步操作


上面的例子中https://www.google.com/网站的访问时间过久,我们设置超过10秒取消该任务,这里取消一个异步操作我们使用到CancellationToken和CancellationTokenSource


DownLoadTest类中的方法修改如下:


public async Task DoRunTaskAsync(string url, CancellationToken ct)

{

    if (ct.IsCancellationRequested)

    {

        Debug.WriteLine(string.Format("取消{0}的运行 :{1,4:N0}ms", url, watch.Elapsed.TotalMilliseconds));

        return;

    }

    Debug.WriteLine(string.Format("下载{0}开始运行 :{1,4:N0}ms", url, watch.Elapsed.TotalMilliseconds));

    WebClient wc = new WebClient();

    await Task.Run(() =>

    {

        var task = wc.DownloadStringTaskAsync(url);

        while (!task.IsCompleted)

        {

            if (ct.IsCancellationRequested)

            {

                Debug.WriteLine(string.Format("取消{0}的运行 :{1,4:N0}ms", url, watch.Elapsed.TotalMilliseconds));

                return;

            }

        }

        if (task.IsCompleted)

            Debug.WriteLine(string.Format("下载{0}运行结束 :{1,4:N0}ms", url, watch.Elapsed.TotalMilliseconds));

    });

}

main函数中如下的方法调用:


CancellationTokenSource cts = new CancellationTokenSource();

CancellationToken token = cts.Token;

var task1 = dwtest.DoRunTaskAsync("https://stackoverflow.com/", token);

var task2 = dwtest.DoRunTaskAsync("https://github.com/", token);

var task3 = dwtest.DoRunTaskAsync("https://www.google.com/", token);

Debug.WriteLine("task.Result等待结果打印");

Thread.Sleep(10000); //等待10秒

Console.WriteLine(task1.Status);

Console.WriteLine(task2.Status);

Console.WriteLine(task3.Status);

cts.Cancel();//超过10s时间取消运行

Console.WriteLine("IsCancellationRequested=" + token.IsCancellationRequested);

Debug.WriteLine(string.Format("主线程运行结束"));

最后得到的结果如下:


下载https://stackoverflow.com/开始运行 :   2ms

下载https://github.com/开始运行 :  61ms

下载https://www.google.com/开始运行 :  62ms

task.Result等待结果打印

下载https://github.com/运行结束 :1,810ms

下载https://stackoverflow.com/运行结束 :1,922ms

取消https://www.google.com/的运行 :10,093ms

主线程运行结束


CancellationToken对象包含一个任务是否应被取消的信息IsCancellationRequested。根据CancellationTokenSource中的Cancel()方法可以设置IsCancellationRequested为true,CancellationToken是不可逆的,设置IsCancellationRequested为true就不能更改了。

扩展阅读