C# 异步编程的简单理解

在你了解异步编程或者并行编程之前,你写的所有代码可能都是同步的。同步什么意思呢?同步是程序的默认执行方式,比如你早上起来,先看手机消息,再吃饭,再坐公交车上班。如果一件一件的做下去,就是同步执行。而异步执行则更符合我们的办事方式——我们通常边吃饭边看手机,边坐车边玩手机。

目录

1.同步程序示例

2.异步程序

3.异步方法的语法特性

3.1 关于Task 和Task

4. 我是坏学生,我就要在上课打游戏

4.异步方式的执行流程

 5. await表达式

6.在调用方法中同步等待任务 

7.在调用方法中异步等待任务


 

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关键字,这两个关键字可以创建并使用异步方法。其结构如下:

  1. 调用方法:该方法调用异步方法,然后在异步方法(可能在相同的线程,也可能在不同的线程)执行其任务的时候继续执行。
  2. 异步方法:该方法异步执行其工作,然后立即返回到调用方法
  3. 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.异步方法的语法特性

  1. 方法头中包含async修饰符
  2. 包含一个或多个await表达式,表示可以异步执行的任务
  3. 必须具备以下三种返回类型:
void Task Task<T>

第二个和第三个的返回对象表示将在未来完成的工作,调用方法和异步方法可以继续执行。

4.异步方法的参数可以是任意类型任意数量,但不能为out和ref参数。

5.按照约定异步方法的名称应该以Async做后缀

6.除方法外,lambda表达式和匿名方法也可以作为异步对象

C# 异步编程的简单理解

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.异步方式的执行流程

异步方法的结构包含三块区域。

C# 异步编程的简单理解

 执行过程如下:

 

 


  •  

 

 

 

同时调用方法中的代码将继续其进程,从异步方法获取Task对象。当需要其实际值时,就引用Task对象的Result属性。

 过程如下图所示:

 

  • 异步执行await表达式的空闲工作
  • 当await表达式执行完成后,执行后续部分,后续部分本省也可能包含await表达式,这些表达式也按照同样的方式处理,即异步执行await表达式,然后执行后续部分。
  • 当后续部分遇到return语句或者到达方法末尾时,如果方法返回类型为void,控制流将推出;如果返回类型为Task,后续部分设置Task属性并推出;如果返回类型为Task<T>,后续部分还将设置Task对象的result属性。

C# 异步编程的简单理解

 

 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对象。