异步编程
异步编程是一种重要的并发编程模型,它对并发任务的控制比多线程模型灵活的多,而且上手更简单,相比困难的多线程并发编程,异步编程模型下新手也很容易写出健壮性、性能都不错的代码,当然异步模型在底层可能是操作系统直接支持,也可能是轮询和线程池模拟的,但这并不重要,我们通常不必关心其底层实现。如果熟悉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);
}
}
}
代码中我们创建了t1
、t2
、t3
,共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
关键字修饰的方法被称为异步方法,异步方法可以返回Task
或Task<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能够使用类似同步代码的方式编写异步代码,既能使用异步编程模型,又能保持同步代码模式的简洁性,这样代码可读性比起嵌套任务就大大提高了。