龙空技术网

C#异步编程之async和await的使用

码农小灰灰 404

前言:

现在兄弟们对“c异步编程”大约比较珍视,兄弟们都想要知道一些“c异步编程”的相关知识。那么小编也在网络上汇集了一些关于“c异步编程””的相关资讯,希望咱们能喜欢,各位老铁们一起来了解一下吧!

1.什么是异步方法

语法上有如下特征:

方法使用async作为修饰符方法内部包含一个或者多个await表达式方法返回类型必须是 void 、Task 、Task<T>三者中之一异步方法的参数可以任意类型,但是不能为out和ref参数约定俗成,一般异步方法都是以 Async作为后缀的。除了方法之外,Lambda表达式和匿名函数也可以作为异步对象。

static void Main(string[] args){ Task<int> t = DoSumAsync(1, 2); Console.WriteLine("结果:{0}", t.Result); Console.ReadKey();}//2.异步方法public static async Task<int> DoSumAsync(int a, int b){ //3.await 表达式 int sum = await Task.Run(() => { return a + b; }); return sum;}

返回类型详解:

Task类型:如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,可以返回一个Task,此时就算异步方法中出现了return语句,也不会返回任何东西(其实都不需要显示使用return语句)。Task<T>类型,除了上面Task的功能,还可以通过 Return属性来返回T类型的值。void类型:如果仅仅是执行异步方法,而不需要与它做任何进一步的交互(“调用并忘记”),此时可以用void,和Task一样,就算有return语句,也得不到任何东西(其实都不需要显示使用return语句)。注意,返回值为void的异步函数前面不能使用await。而调用其他两种返回值的异步方法时,如果没有用await,编译器会给出警告。

返回类型使用原则:

对于需要返回对象的方法,使用Task<T>当一个方法属于触发后不用理会什么时候完成的方法,可以使用void,例如事件处理函数(Event Handler)当虽然不需要返回结果,但却需要知道是否执行完成的方法时,返回一个Task。例如异步方法涉及数据更新逻辑,而调用该异步方法的方法在需要在调用异步方法后读取最新数据,这种情况需要用Task返回类型,而不能用void,否则可能读取不到最新数据。

异步方法的创建和使用三个部分组成:

调用方法(calling method):该方法调用异步方法,在异步方法执行其任务的时候继续执行异步方法(async)await表达式:用于异步方法内部,指明需要异步执行的内容。一个异步方法可以包含任意多个await表达式,如果一个都不包含编译器会发出警告

异步方法由三部分组成:

await之前的部分await表达式后续部分:await之后的部分(如果有多个await,那就是await之间和最后一个await之后的部分)

这里有几个需要注意的问题:

await之前的部分是同步执行的(确切说是第一个await之前)当达到awati的时候,会将异步方法的控制返回给调用方法。如果方法返回的类型是Task或者Task<T>,将创建一个Task对象(表示需异步完成的任务和后续)返回到调用方法。 这里的返回值并不是await表达式的返回值,而是异步方法中声明的返回值类型(即Task或者Task<T>)异步方法内部需要完成以下工作:异步执行await表达是的空闲任务当await表达式执行完成之后,执行后续部分。后续本身也可能是await表达式,处理过程和上一个一致。后续部分如果遇到 return或者方法达到末尾,将做如下的事情:(这个点要注意下,并不是遇到return或者达到方法末尾,就能获取到返回值,它只是退出了(异步任务可能还没执行完毕呢))如果返回的类型是void,控制流就退出了如果返回的类型是Task,后续部分设置Task对象的属性并退出如果返回的类型是Task<T>,不仅要设置Task对象属性,还要设置Task对象的Result属性调用方法继续执行,根据第二步获取Task对象(void除外),当需要其实际值的时候,就引用Task对象中的Result属性。届时,如果异步方法设置了该属性,调用方法获取其值并继续。否则就等待该属性被设置,然后再继续执行。

2.异步方法在非UI线程执行

static void Main(string[] args){ MethodAsync1(); Console.Read();}static async void MethodAsync1(){ Console.WriteLine("当前主线程ID为:" + Thread.CurrentThread.ManagedThreadId ); Console.WriteLine("是否为线程池线程:" + Thread.CurrentThread.IsThreadPoolThread); await Task.Run(() => { Console.WriteLine("第一个await当前线程ID为:" + Thread.CurrentThread.ManagedThreadId ); Console.WriteLine("是否为线程池线程:" + Thread.CurrentThread.IsThreadPoolThread); }); Console.WriteLine("第一个await结束后当前线程ID为:" + Thread.CurrentThread.ManagedThreadId); Console.WriteLine("是否为线程池线程:" + Thread.CurrentThread.IsThreadPoolThread); await Task.Run(() => { Console.WriteLine("第二个await当前线程ID为:" + Thread.CurrentThread.ManagedThreadId ); Console.WriteLine("是否为线程池线程:" + Thread.CurrentThread.IsThreadPoolThread); }); Console.WriteLine("第二个await结束后当前线程ID为:" + Thread.CurrentThread.ManagedThreadId ); Console.WriteLine("是否为线程池线程:" + Thread.CurrentThread.IsThreadPoolThread);}

该演示是在控制台应用程序中完成的,我们可以看到,主线程的ID为10,第一个await和紧接着之后的代码的线程ID为6和11,第二个await和紧接着之后的代码的线程ID为6,在非UI的线程中执行async异步方法,await等待的异步操作和之后接着要执行的代码,都是从线程池中获取了一个线程来执行代码,并且从线程池中获取的也不一定是同一个线程。

3.异步方法在UI线程执行

private void button1_Click(object sender, EventArgs e){ MethodAsync1();}string message;async void MethodAsync1(){ message += ("当前主线程ID为:" + Thread.CurrentThread.ManagedThreadId + "\r\n"); message += ("是否为线程池线程:" + Thread.CurrentThread.IsThreadPoolThread + "\r\n"); await Task.Run(() => { message += ("第一个await当前线程ID为:" + Thread.CurrentThread.ManagedThreadId + "\r\n"); message += ("是否为线程池线程:" + Thread.CurrentThread.IsThreadPoolThread + "\r\n"); }); message += ("第一个await结束后当前线程ID为:" + Thread.CurrentThread.ManagedThreadId + "\r\n"); message += ("是否为线程池线程:" + Thread.CurrentThread.IsThreadPoolThread + "\r\n"); await Task.Run(() => { message += ("第二个await当前线程ID为:" + Thread.CurrentThread.ManagedThreadId + "\r\n"); message += ("是否为线程池线程:" + Thread.CurrentThread.IsThreadPoolThread + "\r\n"); }); message += ("第二个await结束后当前线程ID为:" + Thread.CurrentThread.ManagedThreadId + "\r\n"); message += ("是否为线程池线程:" + Thread.CurrentThread.IsThreadPoolThread + "\r\n"); this.richTextBox1.Text = message;}

这个演示可以看到,在UI线程中使用async异步方法的时候,await后紧接着的代码,一直都会是在UI线程中执行。因此,在使用的时候需要注意这一点,在UI与非UI线程中执行async异步方法的时候,需要注意这两个细节,否则将会带来不必要的麻烦,例如:

static void Main(string[] args){ var a = MethodAsync1(); Console.WriteLine(a.Result); Console.Read();}static async Task<int> MethodAsync1(){ return await Task.Run(() => { Thread.Sleep(1000); return 1; });}

如代码所示,定义一个Task类型返回值的异步方法,先执行这个异步方法MethodAsync1(),然后获取该异步方法的返回值,在这个获取返回值的过程中,执行异步方法的原来的线程也会一直阻塞直到等到获取到返回值,毫无意外的程序在运行一秒钟以后,输出1。但是在UI线程时,如下所示:

private void button1_Click(object sender, EventArgs e){ var a = MethodAsync1(); richTextBox1.Text = a.Result.ToString();}async Task<int> MethodAsync1(){ await Task.Run(() => { Thread.Sleep(1000); }); return 1;}

同样的是定义一个带Task类型返回值的异步方法,同样的也是先执行这个异步方法MethodAsync1(),然后获取该异步方法的返回值,但是,在这里,我们使用了WinForm程序来执行,该异步方法是在UI线程调用的,那么,现在就出现问题了,一旦点击button1按钮,程序便卡死了。

上面说过了,在UI线程中调用的异步方法,在其await操作之后的一些代码,仍然实在UI线程中执行的,那么,首先,点击button1按钮按钮,异步方法执行await中的异步任务,调用函数继续执行到 richTextBox1.Text = a.Result.ToString()这句被阻塞UI线程等待异步结果,而await任务执行完以后,return 1这个操作也需要在UI线程中执行,但UI已经被阻塞了,无法执行任何代码,就这样,我在等你返回值,你在等我释放UI线程来给你执行代码,谁也不相让,造成了程序的卡死。

其实上面的写法有问题,下面正确的写法可以避免卡死:

private void button1_Click(object sender, EventArgs e){ var t = Task.Run(() => { Thread.Sleep(1000); return 1; }); richTextBox1.Text = await t;}

4.异常处理

Async Task或 Aync Task<T> 意味着该方法实际上会自动返回对正在进行的操作的引用,然后您可以在其他地方等待它。在这个返回的引用中,包括异常对象,以及我们等的结果和状态。而async void,没有Task对象,并不能返回这些内容。所以两着的异常处理有区别。

1.Async Task或 Aync Task<T>的异常流

以上代码的运行结果如下:

异常从 async Task ThrowException() 再次抛出后,在调用函数AwaitException_Event()处再被截获,这是我们期望的结果。

2.task.wait()的异常流

将代码改为如下,调用ThrowException这个Task时,不使用await,而使用wait(),如下

那么便得到这样的结果:

这是因为异常在Task内部都保存在AggregateException对象里。每一个Task都会存一个异常列表。当你await 一个task时,第一个异常会重新抛出来,所以你可以捕获指定的异常类型(比如InvalidOperationException)。但是,当你使用Task.Wait或者Task.Result同步地阻塞Task时,所有的异常都被封装在AggregateException抛出来。可见 await task时,异常处理会更容易。

如果在20行不用await,编译程序,有以下两个提醒:

warning CS4014: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

warning CS1998: This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

运行时发现结果不一样了,发现收不到异常了。异常当然不会凭空消失,但是如果不用await,这个异常就像被“吞噬”一样。这是因为,调用程序没有await,没有得到这个Task,同样无法得到这个Task中的异常,异常无法回到调用代码的上下文环境。

可见,调用async Task一定要使用 await。

3.async void的异常流

将ThrowException的返回类型改为async void。那么此时编译器也会告诉不能在调用ThrowException的地方使用await。

运行后,我们得到这个结果,异常没有被“吞噬”,而是直接抛出来了。在AwaitException_Event中的try…catch并不能catch到。这是因为这个方法没有Task,在AwaitException_Event中我们也并不能await,所以异常就在程序当前上下文直接抛出来。因为这是控制台程序,如果在GUI(如WPF和UWP)程序中,在全局的UnHandled_Exception中,我们可以收到这个异常。但是如果有多个这样的async void抛出类似这种异常,我们并不能区分以有效处理。可见使用async void是非常危险的行为, 代码中也避免使用async void (除了异步事件或委托)。

以上三种情况可得,在async,await中,最佳的异常处理就是异步函数使用async Task或者async Task<T> ,使得调用者可以通过await得到函数的引用,获得期间的结果和异常信息。

标签: #c异步编程