C#沉淀-异步编程 一

作者: 东南有大树 | 来源:发表于2018-09-29 15:00 被阅读92次

    什么是异步

    任务以固定的顺序被执行叫做同步,任务不按固定顺序执行则叫做异步

    关于进程与线程

    启动程序时,系统会在内存中创建一个新进程

    进程是构成运行程序的资源的集合

    这些资源包括虚地址空间、文件句柄和许多其他程序运行所需的东西

    在进程内部,系统创建了一个称为线程的内核对象,它代表了真正执行的程序

    Main方法是程序的入口,在这里,程序会开始线程的执行

    要点:

    • 默认情况下,一个进程只包含一个线程从程序的开始一直执行到程序的结束
    • 线程可以派生其他线程,因此在任意时刻,一个进程都可以包含不同状态的多个线程,来执行程序的不同任务
    • 如果一个进程拥有多个线程,它们将共享进程的资源
    • 系统为处理器执行所规划的单元是线程,不是进程

    示例一,不使用异步:

    using System;
    using System.Net;
    using System.Diagnostics;
    
    namespace CodeForAsync
    {
        class MyDownloadString
        {
            //Stopwatch类可以用来计算资源耗时
            Stopwatch sw = new Stopwatch();
    
            public void DoRun()
            {
                const int LargeNumber = 6000000;
                sw.Start();//启动计时
                
                //调用两次下载资源的方法,测算时间消耗
                //下载百度资源
                int t1 = CountCharacters(1, "http://baidu.com");
                //下载搜狗资源
                int t2 = CountCharacters(2, "https://pinyin.sogou.com/");
                
                //调用四次循环方法,测算时间消耗
                CountToALargeNumber(1, LargeNumber);
                CountToALargeNumber(2, LargeNumber);
                CountToALargeNumber(3, LargeNumber);
                CountToALargeNumber(4, LargeNumber);
    
            }
    
            //下载网站资源
            private int CountCharacters(int id, string uristring)
            {
                //实例化一个WebClient对象,用于网站交互
                WebClient wc1 = new WebClient();
                Console.WriteLine("下载{0}开始运行 - 时间节点:{1} ms", id, sw.Elapsed.TotalMilliseconds);//sw.Elapsed.TotalMilliseconds表示一个毫秒级的时间跨度值
    
                //将请求资源下载为string格式
                string result = wc1.DownloadString(new Uri(uristring));
                Console.WriteLine("\t下载{0}运行结束 - 时间节点:{1} ms", id, sw.Elapsed.TotalMilliseconds);
    
                return result.Length;
            }
    
            //循环指定次数,不做任何操作
            private void CountToALargeNumber(int id, int value)
            {
                for (long i = 0; i < value; i++);
                Console.WriteLine("循环{0}运行结束 - 时间节点:{1} ms", id, sw.Elapsed.TotalMilliseconds);
            }
        }
        class Program
        {
            static void Main(string[] args)
            {
                MyDownloadString ds = new MyDownloadString();
                ds.DoRun();
    
                Console.ReadKey();
            }
        }
    }
    

    运行结果:

    下载1开始运行 - 时间节点:2.1404 ms
            下载1运行结束 - 时间节点:413.6084 ms
    下载2开始运行 - 时间节点:413.7037 ms
            下载2运行结束 - 时间节点:927.1802 ms
    循环1运行结束 - 时间节点:949.9849 ms
    循环2运行结束 - 时间节点:973.1916 ms
    循环3运行结束 - 时间节点:995.1234 ms
    循环4运行结束 - 时间节点:1017.0343 ms
    

    从上例可以看出,执行的顺序是:下载1开始-下载1结束》下载2开始-下载2结束》循环1》循环2》循环3》循环4;它们的执行是有序的,如果上一步的任务未执行完毕,下一个任务是无法开始的

    这里的循环并未消耗多少时间,而从网站下载资源的两个任务都消耗了比较多的资源,也就是说,在程序运行期间,所有的任务都会被这两个下载任务拖慢进度

    示例二:使用异步

    using System;
    using System.Net;
    using System.Diagnostics;
    using System.Threading.Tasks;
    
    namespace CodeForAsync
    {
        class MyDownloadString
        {
            Stopwatch sw = new Stopwatch();
    
            public void DoRun()
            {
                const int LargeNumber = 6000000;
                sw.Start();
    
                //下载百度资源
                Task<int> t1 = CountCharactersAsync(1, "http://baidu.com");
                //下载搜狗资源
                Task<int> t2 = CountCharactersAsync(2, "https://pinyin.sogou.com/");
                //这里的返回结果将保存为一个Task<int>对象
    
                CountToALargeNumber(1, LargeNumber);
                CountToALargeNumber(2, LargeNumber);
                CountToALargeNumber(3, LargeNumber);
                CountToALargeNumber(4, LargeNumber);
    
            }
    
            //下载网站资源
            private async Task<int>  CountCharactersAsync(int id, string uristring)
            {
                //async为异步关键字
                //Task<int>表示一个可以返回值的异步操作
    
                //实例化一个WebClient对象,用于网站交互
                WebClient wc = new WebClient();
                Console.WriteLine("下载{0}开始运行 - 时间节点:{1} ms", id, sw.Elapsed.TotalMilliseconds);
    
                //将请求资源下载为string格式
                //await表示异步等待返回结果
                string result = await wc.DownloadStringTaskAsync(new Uri(uristring));
                Console.WriteLine("\t下载{0}运行结束 - 时间节点:{1} ms", id, sw.Elapsed.TotalMilliseconds);
    
                return result.Length;
            }
    
            //循环指定次数,不做任何操作
            private void CountToALargeNumber(int id, int value)
            {
                for (long i = 0; i < value; i++);
                Console.WriteLine("循环{0}运行结束 - 时间节点:{1} ms", id, sw.Elapsed.TotalMilliseconds);
            }
        }
        class Program
        {
            static void Main(string[] args)
            {
                MyDownloadString ds = new MyDownloadString();
                ds.DoRun();
    
                Console.ReadKey();
            }
        }
    }
    

    运行结果:(多运行几次,结果可能不一样)

    下载1开始运行 - 时间节点:3.7227 ms
    下载2开始运行 - 时间节点:250.3289 ms
            下载1运行结束 - 时间节点:271.565 ms
    循环1运行结束 - 时间节点:297.6998 ms
    循环2运行结束 - 时间节点:321.2623 ms
    循环3运行结束 - 时间节点:344.1153 ms
    循环4运行结束 - 时间节点:371.043 ms
            下载2运行结束 - 时间节点:637.35 ms
    

    结果分析:与第一个示例比较可以看出,异步的结果很不一样;在先后开启两个下载后,并没有等待下载完成,就直接开始了循环任务,也就是说在下载任务运行的同时,仍然可以操作别的任务;示例一所有任务完成后的时间节点是1017.0343 ms,而这里所有任务完成后的时间节点是637.35 ms,节省了将近一半的时间

    接下来将详细讲解关于异步的知识

    C# 5.0引入了一个用来构建异步方法的新特性——anync/await,还有一些异步特性没有包含在C#中,而是放在了.NET框架里

    async/await 特性的结构

    异步的方法会在处理完成之前就返回到调用方法

    async/await的特性:

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

    异步语法

    调用方法

    Task<int> value = SomeClass.FuncAsync(1, 2);  //这被称作是调用方法
    

    异步方法

    static class SomeClass
    {
        //被async标识的方法为一个异步方法
        public static async Task<int> FuncAsync(int x, int y)
        {
            //异步方法内部至少有一个await表达式
            int sum = await Task.Run(() => x + y);
            return sum;
        }
    }
    

    Task.Run()表式开启一个异步任务,参数是一个委托,这里使用了Lambda表达式

    异步方法解析

    异步方法在完成其工作的之前即返回到调用方法,然后在调用方法继续执行的时候完成其他工作

    在语法上,异步方法具有如下特点:

    • 方法头中包含async方法修饰符
    • 包含一个或多个await表达式,表示可以异步完成的任务
    • 必须具备以下三种返回类型(TaskTask<T>所返回的对象表示将在未来完成的工作,调用方法和异步方法可以继续执行
      • void
      • Task
      • Task<T>
    • 异步方法的参数可以为任意类型任意数量,但不能out和ref参数
    • 按照约定,异步方法的名称应该以Async为后缀
    • 除了方法以外,Lambda表达式和匿名方法也可以作为异步对象
    //方法拥有async关键字
    //这里的返回类型为Task<T>类型
    async Task<int> FuncAsync(int x, int y)
    {
        //await 表达式
        int sum = await Task.Run(() => x + y);
        //返回语句
        return sum;
    }
    

    异步方法在方法头中必须包含async关键字,而且必须出现在返回类型之前

    async是个修饰符,只表示该方法将包含一个或多个awai表达式,也就是说,async本身并不能创建异步操作

    async是一个上下文关键字,也就是说除了用途方法修饰符之外,async还可用作标识符

    Task<T>:如果调用方法要从调用中获取一个T类型的值,异步方法的返回类型就必须是Task<T>

    调用方法通过读取Task的Result属性来获取这个T类型的值

    任何返回Task<T>类型的异步方法其返回值必须是T类型或可以隐式转换为T的类型

    示例:

    Task<int> value = SomeClass.FuncAsync(1, 2);  //这被称作是调用方法
    int re = value.Result; //获取T类型的值
    

    Task:如果调用方法不需要从调用中返回某个值 ,便需要检查异步方法的状态,那么异步方法可以返回一个Task类型的对象。这时,即使异步方法中出现了return语句,也不会返回任何东西

    示例:

    Task someTask = SomeClass.FuncAsync(1, 2);  //这被称作是调用方法
    someTask.Wait();
    

    void: 如果调用方法仅仅想执行异步方法,而不需要与它做任何进一步的交互时【这称为“调用并忘记”】,异步方法可以返回void类型,这时,即使异步方法中有return语句,也不会返回任何东西

    异步就去的控制流

    异步方法的结构包含三个不同的区域

    • 第一部分为await表达式之前的代码
    • 第二部分为awiat表达式
    • 第三部分为await表达式之后的代码

    如下图示例中,蓝色框为第一部分,绿色框为第二部分,黄色框为第三部分;因为先执行await表达式,再执行赋值语句,所以string result=属于第三部分

    通过这个图例分析得出,因为await会执行一个异步任务,所以,它之前的代码也就是第一部分代码最好是简短而耗时短的任务,以便以更快的速度执行await表达式

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

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

    亲自测试上面的流程:

    using System;
    using System.Net;
    using System.Diagnostics;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace CodeForAsync
    {
        class Program
        {
            static int id = 0;
            //定义一个异步方法,返回Task<int>类型对象
            static async Task<int> fun()
            {
                //执行第一部分代码即await表达式之前
                Console.WriteLine("步骤{0}异步方法内部:第1部分", ++id);
    
                //执行第二部分代码
                //当遇到await表达的时候,会返回到调用方法
                int x = await Task.Run(() =>
                {
                    Thread.Sleep(1000 * 3);//消耗3秒钟
                    Console.WriteLine("步骤{0}异步方法内部:第2部分", ++id);
                    return 0;
                });
    
                //执行第三部分代码 
                Console.WriteLine("步骤{0}异步方法内部:第3部分", ++id);
                return x;
    
            }
            static void Main(string[] args)
            {
                Console.WriteLine("步骤{0}异步方法外,准备开始调用异步方法", ++id);
    
                Task<int> y = fun();
    
                Console.WriteLine("步骤{0}异步方法外,调用异步方法之后", ++id);
    
                Console.WriteLine("步骤{0}访问异步方法的返回值:{1}", ++id, y.Result);
    
                Console.ReadKey();
            }
        }
    }
    

    输出:

    步骤1异步方法外,准备开始调用异步方法
    步骤2异步方法内部:第1部分
    步骤3异步方法外,调用异步方法之后
    步骤5异步方法内部:第2部分
    步骤6异步方法内部:第3部分
    步骤4访问异步方法的返回值:0
    

    解析:

    • 步骤1是在调用异步方法外发生的,这个没有什么疑问
    • 步骤2是在调用了异步方法后,执行了异步方法内的await表达式之前的代码
    • 步骤3是在异步方法遇到await表达式后,因为需要耗时3秒,所以直接返回到了调用方法,也就是到了异步方法之外,继续执行异步方法之外的代码,同时异步方法awiat表达式所指定的代码也在执行
    • 步骤5与步骤6是消耗完3秒后输出的内容,表示异步的内容也执行完了
    • 重点,为什么步骤4会在最后输出呢?首先步骤4是异步方法外的代码,所以当异步方法内部遇到await返回调用方法,依次被执行的是步骤3和步骤4,步骤3很快被输出没有问题,但步骤4使用了异步方法的返回类型中的值,而这个值必须在3秒消耗完后才有,如果在异步方法执行期间没有得到返回值,调用Task<int>去访问int类型的值,便会一直等待下去,所以步骤4是在异步完成后才输出的

    另一个需要注意的地方是,异步方法的返回值并不是一个Task<int>类型的,而是一个int类型的值,这里将其进行了隐式的转换;如果返回类型是Task类型,return并不会返回任何值,只是退出了异步方法

    await表达式

    await表达式指定了一个异步执行的任务

    await 后面的是一个空闲对象(称为任务),这个任务可能是一个Task类型的对象,也可能不是。默认情况下,这个任务在当前 线程异步运行

    await task
    

    一个空闲对象即是一个awaitable类型的实例

    awaitable类型指的是包含GetAwaiter方法的类型,该方法没有参数,返回一个称为awaiter类型的对象。awaiter类型包含以下成员 :

    • bool IsCompleted {get;}
    • void OnCompleted (Action);
    • void GetResult();
    • T GetResult();

    对于awaitalbe类型,无须息构建,只要使用Task即可,它也是awaitable类型的

    Taks.Run()可以创建一个Task,它会在不同的线程上运行你的方法

    Taks.Run()的签名如下:

    Task.Run(Func<TReturn> func)
    

    因此可以将一个泛型委托传进去

    不过,Task.Run()具有很多重载,这里不一一详解,我们举一反三即可

    示例,使用4个Task.Run()重载:

    using System;
    using System.Net;
    using System.Diagnostics;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace CodeForAsync
    {
        static class MyClass
        {
            public static async Task DowWorkAsync()
            {
                //public static Task Run(Action action);
                await Task.Run(() => Console.WriteLine("5"));
    
                //public static Task<TResult> Run<TResult>(Func<TResult> function);
                Console.WriteLine((await Task.Run(() => "6")).ToString());
    
                //public static Task Run(Func<Task> function);
                await Task.Run(() => Task.Run(()=>Console.WriteLine("7")));
    
                //public static Task<TResult> Run<TResult>(Func<Task<TResult>> function);
                int value = await Task.Run(() => Task.Run(() => 8));
                Console.WriteLine(value.ToString());
            }
        }
        
        class Program
        {
    
            static void Main(string[] args)
            {
                Task t = MyClass.DowWorkAsync();
                t.Wait();
                Console.WriteLine("---");
    
                Console.ReadKey();
            }
        }
    }
    

    输出:

    5
    6
    7
    8
    ---
    

    取消一个异步操作

    System.Threading.Tasks命名空间中有两个类是为此目的而设计的:CancellationTokenCancellationTokenSource

    • CancellationToken对象包含一个任务是否被取消的信息
    • 拥有CancellationToken对象的任务需要定期检查其令牌(token)状态。如果CancellationToken对象的IsCancellationRequested属性为true,任务需停止其操作并返回
    • CancellationToken是不可逆的,并且只能使用一次。也就是说,一旦IsCancellationRequested属性被设置为true,就不能更改了
    • CancellationTokenSource对象创建可分配给不同任务的CancellationToken对象。任何持有CancellationTokenSource的对象都可以调用其Cancel方法,这会将CancellationTokenIsCancellationRequested属性设置为true

    示例:

    using System;
    using System.Net;
    using System.Diagnostics;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace CodeForAsync
    {
        class MyClass
        {
            public async Task RunAsync(CancellationToken ct)
            {
                if (ct.IsCancellationRequested)
                    return;
    
                await Task.Run(()=>CycleMethod(ct),ct);
            }
    
            void CycleMethod(CancellationToken ct)
            {
                Console.WriteLine("Starting CycleMethod");
    
                const int max = 10;
                for (int i = 0; i < max; i++)
                {
                    if (ct.IsCancellationRequested)
                        return;
                    Thread.Sleep(1000);
                    Console.WriteLine("    {0} of {1} iterations completed", i+1, max);
                }
            }
        }
        
        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);
                cts.Cancel();//3秒后将token的IsCancellationRequested改为true
    
                t.Wait();
                Console.WriteLine("Was Cancelled: {0}",token.IsCancellationRequested);
    
                Console.ReadKey();
            }
        }
    }
    
    

    输出:

    Starting CycleMethod
        1 of 10 iterations completed
        2 of 10 iterations completed
        3 of 10 iterations completed
    Was Cancelled: True
    

    相关文章

      网友评论

        本文标题:C#沉淀-异步编程 一

        本文链接:https://www.haomeiwen.com/subject/rmtboftx.html