C#沉淀-异步编程 一

什么是异步

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

关于进程与线程

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

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

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

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

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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容