异步编程

异步编程是一种重要的并发编程模型,它对并发任务的控制比多线程模型灵活的多,而且上手更简单,相比困难的多线程并发编程,异步编程模型下新手也很容易写出健壮性、性能都不错的代码,当然异步模型在底层可能是操作系统直接支持,也可能是轮询和线程池模拟的,但这并不重要,我们通常不必关心其底层实现。如果熟悉JavaScript语言就会明白,尤其是类似需要进行异步网络请求的GUI程序,这类程序如果直接使用多线程模型来实现是多么的麻烦。C#语言引入了Task对象来实现异步编程。

另外一点值得注意的是,异步编程的一个小缺点是很容易陷入回调地狱(callback hell),这个问题的解决方案之一是Promise模型,更进一步可以在语法层面实现async/await语法。C#5.0也引入了async/await语法,配合Task能够简化异步代码嵌套回调的编写。

Task对象

C#中的Task对象封装了异步调用,我们可以使用Task对象配合lambda表达式创建一个异步任务,并发创建异步任务,任务就会并发执行,使用非常简单。这里我们简单介绍Task对象的使用,详细内容可以参考并发编程相关章节。

创建异步任务

下面例子中,我们创建了1个异步任务,并调用执行。

// 打印主线程ID
Console.WriteLine("Main thread name: {0}", Thread.CurrentThread.ManagedThreadId);
// 创建异步任务
Task task = new Task(() =>
{
    // 打印异步任务线程ID
    Console.WriteLine("Task thread name: {0}", Thread.CurrentThread.ManagedThreadId);
    Console.WriteLine("任务执行完毕");
});
// 启动异步任务
task.Start();
// 主线程等待异步任务执行完成
task.Wait();

代码中,我们使用new关键字创建了一个Task对象,其构造函数接收一个事件参数,我们这里直接传入一个lambda表达式,其中包含我们异步任务的内容。创建完成后,我们调用task.Start()方法启动异步任务。最终,我们使用task.Wait()方法,在主线程中阻塞等待该任务执行完成。

此外,创建和执行task对象,其实可以合并为一步Task.Run()方法,它接收一个事件参数,能够直接创建并启动任务,实际开发中这种写法更加常用。

并发异步调用例子

下面例子我们并发调用了3个任务。

using System;
using System.Threading.Tasks;

namespace Gacfox.Demo.DemoNetCore
{
    class Program
    {
        static void Main()
        {
            Task t1 = Task.Run(() =>
            {
                // 这里模拟一系列耗时操作
                Thread.Sleep(3000);
                Console.WriteLine("ftp://aaa.com 数据下载完成");
            });
            Task t2 = Task.Run(() =>
            {
                Thread.Sleep(3000);
                Console.WriteLine("ftp://bbb.com 数据下载完成");
            });
            Task t3 = Task.Run(() =>
            {
                Thread.Sleep(3000);
                Console.WriteLine("ftp://ccc.com 数据下载完成");
            });
            Task.WaitAll(t1, t2, t3);
        }
    }
}

代码中我们创建了t1t2t3,共3个异步任务,里面用Thread.Sleep()假装是一个耗时操作(实际开发中这里可能是网络请求、数据处理等)。这些异步任务会在不同的线程中并发执行,我们的主线程使用了Task.WaitAll(t1, t2, t3);方法阻塞等待3个任务执行完成。

async/await 嵌套异步调用

考虑这样一种情况,我们有3个异步任务,C任务需要依赖B任务的返回值,B任务又需要依赖A任务的返回值,那么最终的调用顺序应该是串行A->B->C。此时我们自然能够联想到嵌套使用Task任务对象,但实际上这就陷入了所谓的回调地狱(callback hell)。这种嵌套任务不仅写起来麻烦,而且代码没有任何可读性。此时,我们一定要使用async/await语法。

async/await需要和Task结合使用,async关键字修饰的方法被称为异步方法,异步方法可以返回TaskTask<T>(后者用于带返回值的异步函数),表示方法将以任务形式在未来继续执行;而await关键字可以修饰对另一个异步方法的调用,多个await实际表达了多个异步任务的嵌套执行关系,即上一个任务完成后继续执行下一个。

下面代码例子中,演示了上面这种情况。

using System;
using System.Threading.Tasks;

namespace Gacfox.Demo.DemoNetCore
{
    class MyService
    {
        public async Task DownloadDataAsync(string url)
        {
            await Task.Run(() =>
            {
                // 这里模拟一系列耗时操作
                Thread.Sleep(3000);
                Console.WriteLine("{0} 数据下载完成", url);
            });
        }

        public async Task DownloadAllData()
        {
            await DownloadDataAsync("ftp://aaa.com");
            await DownloadDataAsync("ftp://bbb.com");
            await DownloadDataAsync("ftp://ccc.com");
        }
    }

    class Program
    {
        static void Main()
        {
            MyService service = new MyService();
            service.DownloadAllData().Wait();
            Console.WriteLine("程序运行完成");
        }
    }
}

代码中,MyService类声明了一个async修饰的异步方法DownloadDataAsync,其内部使用await关键字执行了一个异步任务。而DownloadAllData异步方法就是实现了我们上面提到的A->B->C回调顺序。显而易见,async/await能够使用类似同步代码的方式编写异步代码,既能使用异步编程模型,又能保持同步代码模式的简洁性,这样代码可读性比起嵌套任务就大大提高了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。