C# 异步编程的简单理解
在你了解异步编程或者并行编程之前,你写的所有代码可能都是同步的。同步什么意思呢?同步是程序的默认执行方式,比如你早上起来,先看手机消息,再吃饭,再坐公交车上班。如果一件一件的做下去,就是同步执行。而异步执行则更符合我们的办事方式——我们通常边吃饭边看手机,边坐车边玩手机。
目录
1.同步程序示例
这里我们先举个例子:你要上两节历史课,中途会休息10分钟,你会打一局王者荣耀。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace example1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("--------第一节课------");
HistoryClass("公元前221年,秦朝建立。");
Play();
Console.WriteLine("--------第二节课------");
HistoryClass("公元前202年,西汉建立。");
}
public static void Play()
{
Console.WriteLine(" ***本线程ID:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.WriteLine("课间休息10分钟!来一句王者荣耀吧");
Console.WriteLine("激战中...");
Thread.Sleep(3000);
Console.WriteLine("游戏结束"+DateTime.Now.ToString());
}
public static void HistoryClass(string s)
{
Console.WriteLine(" ***本线程ID:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.WriteLine("本节课开始了......"+DateTime.Now.ToString());
Console.WriteLine(s);
Thread.Sleep(5000);
Console.WriteLine("本节课结束!!!!!!"+DateTime.Now.ToString());
}
}
运行结果如下:
--------第一节课------
***本线程ID:1
本节课开始了......2019/4/10 12:04:07
公元前221年,秦朝建立。
本节课结束!!!!!!2019/4/10 12:04:12
***本线程ID:1
课间休息10分钟!来一句王者荣耀吧
激战中...
游戏结束2019/4/10 12:04:15
--------第二节课------
***本线程ID:1
本节课开始了......2019/4/10 12:04:15
公元前202年,西汉建立。
本节课结束!!!!!!2019/4/10 12:04:20
请按任意键继续. . .
从结果可以看到,程序是顺序执行的:第一节历史课-->课间打游戏-->第二节历史课。其实从每个子函数的线程ID就可以看出,该程序始终只有一个线程,所以每个函数只能顺序执行。
2.异步程序
异步程序是什么意思呢,异步的方法在调用程序完成之前就返回到调用程序。就像你先吃饭在玩游戏,但如果你点的外卖,就没必要等到饭送过来再去玩游戏,你可以先玩一两局。
在C#中实现异步主要靠async和await关键字,这两个关键字可以创建并使用异步方法。其结构如下:
- 调用方法:该方法调用异步方法,然后在异步方法(可能在相同的线程,也可能在不同的线程)执行其任务的时候继续执行。
- 异步方法:该方法异步执行其工作,然后立即返回到调用方法
- await表达式:用于异步方法内部,指明需要异步执行的任务。异步异步方法可以包含任意多个await表达式,但是如果一个都不包含吧,编译器会发出警告。
语法形式如下:
调用方法
class Program
{
static void main()
{
...
Task<int> value=DoAsyncStuff.CalculateSumAsync(5,6);
...
}
}
异步方法
static class DoAsyncStuff
{
public static Async Task<int> CalculateAsync(int a,int b)
{
int sum= await Task.Run( ()=> GetSum(a,b)); //await 表达式
return sum;
}
...
}
3.异步方法的语法特性
- 方法头中包含async修饰符
- 包含一个或多个await表达式,表示可以异步执行的任务
- 必须具备以下三种返回类型:
void | Task | Task<T> |
第二个和第三个的返回对象表示将在未来完成的工作,调用方法和异步方法可以继续执行。
4.异步方法的参数可以是任意类型任意数量,但不能为out和ref参数。
5.按照约定异步方法的名称应该以Async做后缀
6.除方法外,lambda表达式和匿名方法也可以作为异步对象
3.1 关于Task<T> 和Task
如果调用方法要从调用的中获取一个T类型的值,异步方法返回类型就必须是Task<T>,调用方法将通过读取Task
的result属性来获取这个T类型的值。下面的代码展示:
Task<int> value=DoStuff.CalculateSumAsync(5,6);
Console.WriteLine("Value:{0}",value.result);
如果调用方法不需要从异步方法中获取返回值,但需要检查异步方法的状态,那么异步方法可以返回一个Task类型的对象,这时,即使异步方法有retrun 语句也不会返回任何东西。
Task SomeTask=DoStuff.CalculateSumAsync(5,6);
SomeTask.Wait();
至于void,可以在不需要和异步方法做进一步的交互时使用。
4. 我是坏学生,我就要在上课打游戏
有了前面的说明,我们就可以对之前的代码进行修改。假如我是个坏学生,一上课我就要打游戏(老师讲他的,我玩我的),那就需要老师的课不影响我游戏的运行,所以我们对历史课这个函数做了修改,修改如下:
public static async void HistoryClass(string s)
{
await Task.Run(() =>
{
Console.WriteLine(" 历史课——本线程ID:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.WriteLine("本节课开始了......" + DateTime.Now.ToString());
Console.WriteLine(s);
Thread.Sleep(5000);
Console.WriteLine("本节课结束!!!!!!" + DateTime.Now.ToString());
});
}
主函数运行结果如下:
主函数线程ID:1
--------第一节课------
历史课——本线程ID:3
打游戏——本线程ID:1
课间休息10分钟!来一句王者荣耀吧
激战中...
本节课开始了......2019/4/10 15:10:53
公元前221年,秦朝建立。
游戏结束2019/4/10 15:10:56
--------第二节课------
历史课——本线程ID:4
本节课开始了......2019/4/10 15:10:56
请按任意键继续. . .
可以发现,历史课所在的线程和主线程不一样,一上历史课,我的游戏就开始了,因为游戏的持续时间是3秒,所以游戏结束了,课还没结束,这很正常,而且因为历史课不再主线程,所以主线程结束了,课还没结束,自然有些信息没有打印出来。
加入将游戏时间改为12秒,再执行一边,结果如下:
主函数线程ID:1
--------第一节课------
历史课——本线程ID:3
打游戏——本线程ID:1
课间休息10分钟!来一句王者荣耀吧
激战中...
本节课开始了......2019/4/10 15:24:12
公元前221年,秦朝建立。
本节课结束!!!!!!2019/4/10 15:24:17
游戏结束2019/4/10 15:24:24
--------第二节课------
历史课——本线程ID:4
本节课开始了......2019/4/10 15:24:24
公元前202年,西汉建立。
请按任意键继续. . .
理论上12秒的游戏时间足够两节课了(一节课是5秒),但是发现在游戏过程中,第一节课结束了第二节课才开始,原因很简单,因为打游戏的方法还不是异步方法,所以会阻塞第二节课的运行,我们再将打游戏变为异步方法,毕竟我打我的游戏,不能影响老师上课啊。修改如下:
public static async void Play()
{
await Task.Run(() =>
{
Console.WriteLine(" 打游戏——本线程ID:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.WriteLine("课间休息10分钟!来一句王者荣耀吧");
Console.WriteLine("激战中...");
Thread.Sleep(12000);
Console.WriteLine("游戏结束" + DateTime.Now.ToString());
});
}
然后再运行看看:
主函数线程ID:1
--------第一节课------
历史课——本线程ID:3
--------第二节课------
打游戏——本线程ID:4
课间休息10分钟!来一句王者荣耀吧
激战中...
历史课——本线程ID:5
请按任意键继续. . .
结果有点出乎意外,不过仔细想想很容易理解,因为三次调用的函数都不在主线程,主线程执行完就结束了,根本不管子函数的死活。
4.异步方式的执行流程
异步方法的结构包含三块区域。
执行过程如下:
同时调用方法中的代码将继续其进程,从异步方法获取Task对象。当需要其实际值时,就引用Task对象的Result属性。
过程如下图所示:
- 异步执行await表达式的空闲工作
- 当await表达式执行完成后,执行后续部分,后续部分本省也可能包含await表达式,这些表达式也按照同样的方式处理,即异步执行await表达式,然后执行后续部分。
- 当后续部分遇到return语句或者到达方法末尾时,如果方法返回类型为void,控制流将推出;如果返回类型为Task,后续部分设置Task属性并推出;如果返回类型为Task<T>,后续部分还将设置Task对象的result属性。
5. await表达式
await表达式指定了一个异步执行的任务。其语法如下所示,由await关键字和一个空闲对象组成。这个任务可能时一个Task类型的对象,也可能不是,默认情况下,这个任务在当前线程异步运行。
await task
一个空闲对象即是一个awaitable类型的实例,awaitable类型是指包含GetAwaiter方法的类型,该方法没有参数,返回一个称为awaiter类型对象。awaiter包含以下成员:
bool IsCompleted {get:}
void OnCompleted(Action);
还包含一下成员之一:
void GetResult();
T GetResult();
然而使劲上,你并不需要构建自己的awaitable。相反,你应该使用Task类,它是awaitable类型,对于awaitable,大多数程序员所需要的就是Task。
最简单的方式是在你的方法中使用Task.Run方法来创建一个Task。关于Task.Run,有一点非常重要,即它是在不同的线程上运行你的方法。Task.Run的一个签名如下:
Task.Run(Func<TResult> func)
因此要将你的方法爨地给Task.Run方法,需要基于该方法创建一个委托。下面给出一个例子展示三种实现方式。
public int Get10()
{
reurn10;
}
public async Task DoWorkAsync()
{
Func<int> ten=new Func<int>(Get10);
int a=await Task.Run(ten);
int b=await Task.Run(new Func<int>(Get10));
int c=await Task.Run( ()=>{return 10;});
Console.WriteLine("{0},{1},{2}",a,b,c};
}
在主函数调用这个函数会得到:10 10 10
6.在调用方法中同步等待任务
前面的例子我们也看到了,如果主函数的所有调用方法都都是异步方法,那么主函数会在其它异步方法结束前就关闭线程,所以无法看到其它的结果。如果我们就像等第一节历史课完了再打游戏,那么我们就可以加入同步等待操作。
Task类提供了一个实例方法wait,可以再Task对象上调用该方法。再次之前我们需要将异步方法的返回类型改为Task,如下所示:
public static async Task HistoryClass(string s)
{
await Task.Run(() =>
{
Console.WriteLine(" 历史课——本线程ID:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.WriteLine("本节课开始了......" + DateTime.Now.ToString());
Console.WriteLine(s);
Thread.Sleep(5000);
Console.WriteLine("本节课结束!!!!!!" + DateTime.Now.ToString());
});
}
然后主函数修改如下:
class Program
{
static void Main(string[] args)
{
Console.WriteLine(" 主函数线程ID:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.WriteLine("--------第一节课------");
Task t1=HistoryClass("公元前221年,秦朝建立。");
t1.Wait();
Task t3=Play();
//t3.Wait();
Console.WriteLine("--------第二节课------");
Task t2=HistoryClass("公元前202年,西汉建立。");
//t2.Wait();
// Thread.Sleep(3000);
}
public static async Task Play()
{
await Task.Run(() =>
{
Console.WriteLine(" 打游戏——本线程ID:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.WriteLine("课间休息10分钟!来一句王者荣耀吧"+DateTime.Now.ToString());
Console.WriteLine("激战中...");
Thread.Sleep(12000);
Console.WriteLine("游戏结束" + DateTime.Now.ToString());
});
}
运行结果如下:
主函数线程ID:1
--------第一节课------
历史课——本线程ID:3
本节课开始了......2019/4/10 18:44:14
公元前221年,秦朝建立。
本节课结束!!!!!!2019/4/10 18:44:19
--------第二节课------
打游戏——本线程ID:4
历史课——本线程ID:3
请按任意键继续. . .
可以看到打游戏和第二节课一定是在第一节课上完才开始。需要说明的是,如果你将
t1.Wait();
放到主函数的最后面,会得到不同的结果:
主函数线程ID:1
--------第一节课------
历史课——本线程ID:3
--------第二节课------
打游戏——本线程ID:4
历史课——本线程ID:5
本节课开始了......2019/4/10 18:48:02
公元前221年,秦朝建立。
本节课开始了......2019/4/10 18:48:02
公元前202年,西汉建立。
课间休息10分钟!来一句王者荣耀吧2019/4/10 18:48:02
激战中...
本节课结束!!!!!!2019/4/10 18:48:07
本节课结束!!!!!!2019/4/10 18:48:07
请按任意键继续. . .
但是不管怎样,在主函数运行结束前,能保证第一节课一定会结束。实验发现,如果等待在函数在异步方法之前会阻塞异步方法,直到等待的异步方法运行完成,但是如果在等待前,其它的异步方法已经开始,则无法阻塞(因为来不及了),所以会各跑各的。
wait方法用于单一的Task对象,你也可以等待一组Task对象,对于一组Task对象,你可以等待所有的任务都结束,也可以等待某一个任务结束,实现这两个功能的是Task类中的两个静态方法:
- WaitAll
- WaitAny
这两个方法是同步方法且没有返回值。他们停止,知道条件满足后再继续执行。用法示例:
Task<int> t1=DoStuffAsync("...");
Task<int> t1=DoStuffAsync("...");
Task<int> tasks=new Task<int>[] {t1,t2};
Task.WaitAll(tasks);
Task.WaitAny(tasks);
WaitAll和WaitAny还分别包含4个重载,如设置超时时间或使用CancellationToken来强制执行后续部分。
7.在调用方法中异步等待任务
如果你希望用await表达式来等待Task,可以通过Task.WhenAll和Task.WhenAny方法来实现,这两个方法称为组合子。
List<Task<string>> tasks=new List<Task<string>>();
tasks.Add(t1);
tasks.Add(t2);
await Task.WhenAll(tasks);
此外还有一个方法:Task.Delay方法。该方法创建一个Task对象,该对象将暂停其在线程中的处理,并在一定时间后完成,然而与Thread.Sleep阻塞线程不同的是,Task.Delay不会阻塞线程,线程可以继续处理其它工作。我们当然可以用Task.Delay函数替代之前的Thread.Sleep:
public static async Task HistoryClass(string s)
{
await Task.Run( async () =>
{
Console.WriteLine(" 历史课——本线程ID:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.WriteLine("本节课开始了......" + DateTime.Now.ToString());
Console.WriteLine(s);
// Thread.Sleep(5000);
await Task.Delay(5000);
Console.WriteLine("本节课结束!!!!!!" + DateTime.Now.ToString());
});
}
注意:lambda表达式需要用async修饰。理论上在等待的5秒中线程可以干别的事情,如果该线程还有其它任务要执行。
Delay方法包含4个重载,可以以不同的方式来指定周期,同时还允许使用CancellationToken对象。