异步(async、await)

同步与异步

同步:如果一个程序调用某个方法,等待其执行所有处理后才继续执行,我们就称这样的方法是同步的,这是默认的形式
异步:异步的方法在处理完成之前就返回到调用方法,C#的async/await特性可以创建并使用异步方法。

async/await特性的组成

(1)调用方法:该方法调用异步方法,然后在异步方法(可能在相同的线程,也可能在不同的线程)执行其任务的时候继续执行
(2)异步(async)方法:该方法异步执行其工作,然后立即返回到调用方法
(3)await表达式用于异步方法内部指明需要异步执行的任务一个异步方法可以包含任意多个await表达式,不过如果一个都不包含的话编译器会发出警告

异步方法的定义规则

(1)方法头中包含async方法修饰符它在返回类型之前。
(2)包含1个或多个await表达式,表示可以异步完成的任务
(3)方法的返回类型voidTaskTask<T>中的一种,Task和Task<T>的返回对象表示将在未来完成的工作,调用方法和异步方法可以继续执行。
void:如果调用方法仅仅想执行异步方法,而不需要与它做任何进一步的交互时,异步方法可以返回void类型,这时即使异步方法中包含任何return语句,也不会返回任何东西。
Task:如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,那么异步方法可以返回一个Task类型的对象,这时,即使异步方法中出现了return语句,也不会返回任何东西

Task<T>:如果方法要从调用中获取一个T类型的值,异步方法的返回类型就必须是Task<T>,调用方法将通过读取Task的Result属性来获取这个T类型的值。

(4)任何返回Task<T>类型的异步方法其返回值必须为T类型或可以隐式转换为T的类型
(5)异步方法的参数可以为任意类型任意数量,但不能为out、ref参数
(6)按照约定,异步方法的名称应该以Async为后缀
(7)除了方法以外,Lambda表达式和匿名方法也可以作为异步对象。

异步方法结构

(1)第一个await表达式之前的部分:从方法开头到第一个await表达式之间的所有代码,这一部分应该只包含少量且无需长时间处理的代码。
(2)await表达式:表示被异步执行的任务
(3)后续部分:在await表达式以后出现的方法中的其余代码,包括其执行环境,如所在线程信息、目前作用域的变量值,以及当await表达式完成后要重新执行所需的其他信息。

异步方法控制流

调用异步方法后,从第一个await表达式之前的代码开始,正常(同步)执行直到遇见第一个await,这一区域实际上在第一个await表达式处结束,此时await任务还没有完成(大多数情况下)。当await任务完成时,方法将继续同步执行,如果还有其他await,就重复上述过程。

当达到await表达式时,异步方法将控制流返回到调用方法,如果方法的返回类型为TaskTask<T>类型,将创建一个Task对象,表示需异步完成的任务和后续,然后将该Task返回到调用方法

目前有俩个控制流:异步方法内和调用方法内的,异步方法内的代码完成以下工作,异步执行await表达式的空闲任务,当await表达式完成时,执行后续部分,后续部分本身也可能包含其他await表达式,这些表达式也将按照相同的方式处理,即异步执行await表达式,然后执行后续部分。当后续部分遇到return语句或到达方法末尾时,如果方法返回类型为void,控制流将退出,如果方法返回类型为Task,后续部分设置Task的属性并退出,如果返回类型为Task<T>,后续部分还将设置Task对象的Result属性。

同时,调用方法中的代码将继续其进程,从异步方法获取Task对象,当需要其实际值时,就引用Task对象的Result属性,届时,如果异步方法设置了该属性,调用方法就能获得该值并继续,否则将暂停并等待属性被设置,然后在执行

    class A
    {
        static public void DoRun()
        {
            var t = foo();
            Console.WriteLine(t.Result);
        }

        static private async Task<int> foo()
        {
            var t1 = Task.Run(() =>
              {
                  Thread.Sleep(100);
                  return 100;   //设置Task对象的Result属性
              });


            var t2 = Task.Run(() =>
              {
                  Thread.Sleep(200);
                  return 200;   //设置Task对象的Result属性
              });

            await Task.WhenAll(new List<Task<int>>() { t1, t2 }); //异步等待集合内的Task都完成,不会占用主线程的时间

            return t1.Result+t2.Result;
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            A.DoRun();
        }
    }

await表达式

await表达式指定了一个异步执行的任务await关键字和一个空闲对象(称为任务)组成,这个任务可能是一个Task类型,也可能不是,默认情况下,这个任务在当前线程异步运行。

一个空闲对象即是一个awaitable类型的实例,awaitable类型是指包含GetAwaiter方法的类型,该方法没有参数,返回一个称为awaiter类型的对象,awaiter类型包含以下成员
(1)bool IsCompleted {get;}
(2)void OnCompleted(Action);
它还包含以下成员之一:
(1)void GetResult();
(2)T GetResult(); (T为任意类型)
实际上,并不需要构建自己的awaitable,相反,应该使用Task类,它是awaitable类型,作为await表达式的任务,最简单的方式是使用Task.Run方法来创建一个Task,关于Task.Run,有一点非常重要,即它是在不同的线程上运行你的方法
Task Run(Func<TReturn> func)   这个是Run函数众多重载版本中的其中一个,参数为一个委托类型,这个委托没有参数,返回值类型为TReturn,因此要将你的方法传递给Task.Run方法时,需要基于该方法创建一个委托。

取消一个异步操作

当我们想要终止异步执行时,可以通过类CancellationToken和类CancellationTokenSource来实现,拥有CancellationToken对象的任务需要定期检查其令牌(token)状态,如果CancellationToken对象的IsCancellationRequested属性为true,任务需停止其操作并返回CancellationToken是不可逆的,并且只能使用一次,也就是说,一旦IsCancellationRequested属性被设置为true,就不能更改了。

CancellationTokenSource对象创建可分配给不同任务的CancellationToken对象,任何持有CancellationTokenSource的对象都可以调用其Cancel方法,这会将CancellationToken的IsCancellationRequested属性设置为true

    class MyClass
    {
        public async Task RunAsync(CancellationToken ct)
        {
            if (ct.IsCancellationRequested)
            {
                Console.WriteLine(1111);
                return;
            }
            await Task.Run(() => CycleMethod(ct), ct);
        }

        void CycleMethod(CancellationToken ct)
        {
            Console.WriteLine("Starting CyclyMethod");
            const int max = 5;
            for (int i = 0; i < max; i++)
            {
                if (ct.IsCancellationRequested)
                {
                    Console.WriteLine("退出");
                    return;     //退出
                }
                Thread.Sleep(1000);
                Console.WriteLine("{0} of {1} iterations completed", i + 1, max);
            }
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken token = cts.Token;

            MyClass mc = new MyClass();
            Task t = mc.RunAsync(token);

            Thread.Sleep(3000);  //等待3秒
            cts.Cancel();        //取消异步

            t.Wait();    //等待这个Task完成执行过程
            Console.WriteLine("Was Cancelled:{0}", token.IsCancellationRequested);
        }
    }

输出为:

Starting CyclyMethod
1 of 5 iterations completed
2 of 5 iterations completed
3 of 5 iterations completed
退出
Was Cancelled:True

t.wait会等待这个Task结束后在继续向下执行,如果没有这行代码,主程序会提前打印最后一样代码,然后退出,这样就会少输出一定内容,这和之前所说的一样,假如异步方法返回的是Task<int>类型,那么如果在主程序中如果使用到该返回的Result字段,则会等待这个值被赋值了之后再继续向下走,否则主程序不会等待异步方法。

在调用方法中同步地等待任务(Wait)

调用方法可以调用任意多个异步方法并接收它们返回的Task对象,然后你的代码会继续执行其他任务,但在某个点上可能需要等待某个特殊Task对象完成,然后再继续,为此Task类提供了一个实例方法Wait,可以在Task对象上调用该方法。
Wait:用于等待单一Task对象
WaitAll(静态方法):用于等待一组Task中所有任务都结束
WaitAny(静态方法):用于等待一组Task中某一个任务结束

在异步方法中异步地等待任务(WhenAll、WhenAny)

有时在异步方法中,会希望用await表达式来等待Task,调用到await时异步方法会返回到调用方法,然后异步方法执行自己的流程,可以通过Task.WhenAllTask.WhenAny来实现等待一个或所有任务完成,然后接着执行后面语句。例如tasks是多个任务的集合,当调用到await Task.WhenAll(tasks); 则等待集合内所有任务都完成再继续走下面的代码,如果换成Task.WhenAny则集合内只要有一个任务完成则就走下面的代码。

Task.Delay方法

Task.Delay方法创建一个Task对象,该对象将暂停其在线程中的处理,并在一定时间之后完成,与Thread.Sleep阻塞线程不同的是,Task.Delay不会阻塞线程,线程可以继续处理其他工作。

Task.Yield

Task.Yield方法创建一个立即返回的awaitable,等待一个Yield可以让异步方法在执行后续部分的同时返回到调用方法。可以将其理解成离开当前的消息队列,回到队列末尾,让处理器有时间处理其他任务。

    static class DoStuff
    {
        public static async Task<int> FindSeriesSum(int i1)
        {
            int sum = 0;
            for(int i=0;i<i1;i++)
            {
                sum += i;
                if (i % 10 == 0)
                {
                    Console.WriteLine("前");
                    await Task.Yield();
                    Console.WriteLine("后");
                }
            }
            return sum;
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            Task<int> value = DoStuff.FindSeriesSum(100);
            Console.WriteLine(111);
            Console.WriteLine(value.Result);
            Console.ReadLine();
        }
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章