C# 异步之Async/await

该文章仅为个人理解,如有错误请指正,标红内容为重点,会有针对异步的多个文章,包含Task,APM(BeginInvoke,EndInvoke)等。

一、基本概念:

  • Async/await 关键字是在.Net 4.5引入的关键字来创建自动延续任务实现异步调用。
  • async/await是语法糖,等价于task.Run().ContinueWith(action,Task.FromCurrentSynchronizationContext())。
  • 延续:是.Net提供一种异步编程模式,用来代替阻塞、等待、轮询异步调用的操作。
  • Task的ContinueWith方法:在task任务完成后继续执行ContinueWith中的代码,除非指定线程,默认情况下ContinueWith中的代码在线程池中的线程运行。
  • Task.ContinueWith指定线程,由于ContinueWith中的代码需要在指定的线程运行例如更新UI控件必须在UI线程中执行,使用方法重载,传递TaskScheduler参数,TaskScheduler.FromCurrentSynchronizationContext()可以捕获调用线程同步上下文,可以保证ContinueWIth中的代码封送给指定的同步上下文。
  • 同步上下文:同步上下文的工作就是确保调用在正确的线程上执行。任何一方都可以提供一个执行的上下文,允许其他第三方向其封送调用。创建Form控件或Form时,会调用父类Control的构造函数会检查当前线程是否拥有同步上下文,如果没有则会安装WindowsFormsSynchronizationContext作为线程的同步上下文。

二、Async/await概念解释:

Async:表示函数包含一个或多个基于任务(Task)的异步方法调用。

返回值:有返回值,修改为Task,没有返回值,修改为Task,如果函数为异步事件处理程序(也就是注册事件),依然使用void。

 await:

对每个任务调用使用await,await之后的代码编译器都会当做先前任务的延续。

当遇到await后会立刻跳出当前方法,直到await中task执行完毕后,await之后代码会自动加入延续。

编译器会为await生成的代码获取当前同步上下文,并创建任务调度器,然后把调度器赋值给延续。

等价于Task.ContinueWith(Action,Task.FromCurrentSynchronizationContext)代码。

也可以将await理解为一个运算符,该运算符表示等待异步执行的结果,也就是对Task.Result属性进行操作,而不是对方法本身进行操作。

三、基本使用:

        //情况1.异步事件处理程序
        private async void btnAsync1_Click(object sender, EventArgs e)
        {
            //开启基于Task的异步方法调用
           string taskResult=await Task.Run(() =>
            {
                Thread.Sleep(1000);
                return "BtnAsync1";
            });
            //延续,await会自动捕获同步上下文(UI线程的同步上下文),不存在跨线程操作Control
            btnAsync1.Text = taskResult;
        }

        //情况2.没有返回值,修改为Task
        private void btnAsync2_Click(object sender, EventArgs e)
        {
            //必须进行接收返回值,因为要捕获异常和Task运行结果
            Task taskResult = SetBtnTxt();
            btnAsync2.Text = "BtnAsync2";
        }

        //情况2.没有返回值,修改为Task
        public async Task SetBtnTxt()
        {
            string taskResult = await Task.Run(() =>
            {
                Thread.Sleep(1000);
                return "BtnAsync2";
            });
            btnAsync2.Text = taskResult;
        }

        //情况3.返回值为string,修改为Task
        private void btnAsync3_Click(object sender, EventArgs e)
        {
            Task taskResult = GetBtnText();
            btnAsync3.Text = "BtnAsync3";
        }

        //情况3.返回值为string,修改为Task
        public async Task GetBtnText()
        {
            string taskResult= await Task.Run(() =>
            {
                Thread.Sleep(1000);
                return "BtnAsync3";
            });
            btnAsync3.Text = taskResult;
            return taskResult;
        }

  四、Async/await和死锁:

情况:

        //发生死锁
        private void btnAsync4_Click(object sender, EventArgs e)
        {
            Task taskResult = GetBtnText();
            //1.出现线程阻塞
            btnAsync4.Text = taskResult.Result;
        }

        public async Task GetBtnText()
        {
            string taskResult=await Task.Run(() =>
            {
                Thread.Sleep(1000);
                return "BtnAsync3";
            });
            //2.出现线程阻塞 
            return taskResult;
        }
            

白话说明:

当代码执跳出GetBtnText后,会执行taskResult.Result会发生UI线程阻塞,而GetBtnText中执行完Task代码后,会由await进行延续Task后的代码,延续代码会由Task中的线程封送到UI线程。

Task的返回值,在执行完延续代码后才会进行返回,延续代码需要UI线程,而UI线程在等待Task的返回值,导致了UI线程和Task线程之间相互等待,发生死锁。

只要使用了await之后,即使没有返回值,也没有延续代码,也会发生延续操作,也必须调用UI线程。

专业术语说明:

使用async和await模式时,就引入了非阻塞执行流程(异步Task)到应用程序中,非阻流程可以启动许多异步任务,在时间上单个线程(UI线程)的执行流程是顺序的,当允许它持续贯穿执行整个应用程序时,非阻塞流程(Task线程)才能达到最佳状态,代码最终释放关联的线程到线程池去执行其他任务,当在线程(UI线程)上初始化非阻塞执行流程、通过等待任务(Task.Result)来显示阻塞线程时,就会阻止非阻塞流程完成(Task线程需要封送延续任务到UI线程上)。
            关联的线程无法完成,直到等待的任务完成,如果应用程序是多线程的,那么当显式阻塞线城市,会导致不必要的系统资源消耗,最终会影响应用程序的总体响应能力。如果应用程序是单线程时(或者具备线程关联性),则在使用async和await启动非阻塞执行流程后选择显式阻塞执行流程时,会产生死锁,因为阻塞了唯一可以继续执行任务的线程。

解决方式:

1.在所有异步交互里使用一致性技术,即Demo中btnAsync4_Click继续使用async/await。

2.使用Task.ConfigureAwait()控制是否让编译器为await生成代码,以捕获上下文。在TaskAsync中Task.Run()函数增加.ConfigureAwait(false),最后为Task.Run().ConfigureAwait(false)。

你可能感兴趣的:(异步与多线程)