C#异步编程由浅入深(二)Async/Await的作用.
考虑到直接讲实现一个类Task库思维有点跳跃,所以本节主要讲解Async/Await的本质作用(解决了什么问题),以及Async/Await的工作原理。实现一个类Task的库则放在后面讲。首先回顾一下上篇博客的场景。
class Program
{
public static string GetMessage()
{
return Console.ReadLine();
}
public static string TranslateMessage(string msg)
{
return msg;
}
public static void DispatherMessage(string msg)
{
switch (msg)
{
case "MOUSE_MOVE":
{
OnMOUSE_MOVE(msg);
break;
}
case "MOUSE_DOWN":
{
OnMouse_DOWN(msg);
break;
}
default:
break;
}
}
public static void OnMOUSE_MOVE(string msg)
{
Console.WriteLine("开始绘制鼠标形状");
}
public static int Http()
{
Thread.Sleep(1000);//模拟网络IO延时
return 1;
}
public static void HttpAsync(Action<int> action,Action error)
{
//这里我们用另一个线程来实现异步IO,由于Http方法内部是通过Sleep来模拟网络IO延时的,这里也只能通过另一个线程来实现异步IO
//但记住,多线程是实现异步IO的一个手段而已,它不是必须的,后面会讲到如何通过一个线程来实现异步IO。
Thread thread = new Thread(() =>
{
try
{
int res = Http();
action(res);
}
catch
{
error();
}
});
thread.Start();
}
public static Task<int> HttpAsync()
{
return Task.Run(() =>
{
return Http();
});
}
public static void OnMouse_DOWN(string msg)
{
HttpAsync()
.ContinueWith(t =>
{
if(t.Status == TaskStatus.Faulted)
{
}else if(t.Status == TaskStatus.RanToCompletion)
{
Console.WriteLine(1);
//做一些工作
}
})
.ContinueWith(t =>
{
if (t.Status == TaskStatus.Faulted)
{
}
else if (t.Status == TaskStatus.RanToCompletion)
{
Console.WriteLine(2);
//做一些工作
}
})
.ContinueWith(t =>
{
if (t.Status == TaskStatus.Faulted)
{
}
else if (t.Status == TaskStatus.RanToCompletion)
{
Console.WriteLine(3);
//做一些工作
}
});
}
static void Main(string[] args)
{
while (true)
{
string msg = GetMessage();
if (msg == "quit") return;
string m = TranslateMessage(msg);
DispatherMessage(m);
}
}
}
在OnMouse_DOWN这个处理函数中,我们使用Task的ContinueWith函数进行链式操作,解决了回调地狱问题,但是总感觉有点那么不爽,我们假想有个关键字await它能实现以下作用:首先await必须是Task类型,必须是Task类型的(其实不是必要条件,后面会讲到)原因是保证必须有ContinueWith这个函数,如果Task没有返回值,则把await后面的代码放到Task中的ContinueWith函数体内,如果有返回值,则把Await后的结果转化为访问Task.Result属性,文字说的可能不明白,看下示例代码
//无返回值转换前
public async void Example()
{
Task t = Task.Run(() =>
{
Thread.Sleep(1000);
});
await t;
//做一些工作
}
//无返回值转换后
public void Example()
{
Task t = Task.Run(() =>
{
Thread.Sleep(1000);
});
t.ContinueWith(task =>
{
//做一些工作
});
}
//有返回值转换前
public async void Example()
{
Task<int> t = Task.Run<int>(() =>
{
Thread.Sleep(1000);
return 1;
});
int res = await t;
//使用res做一些工作
}
//有返回值转换后
public void Example()
{
Task<int> t = Task.Run<int>(() =>
{
Thread.Sleep(1000);
return 1;
});
t.ContinueWith(task =>
{
//使用task.Result做一些工作
});
}
看起来不错,但至少有以下问题,如下:
- 该种转换方法不能很好的转换Try/Catch结构
- 在循环结构中使用await不好转换
- 该实现与Task类型紧密联系
一二点是我自己认为的,但第三点是可以从扩展async/await这点被证明的。但无论怎样,async/await只是对方法按照一定的规则进行了变换而已,它并没有什么特别之处,具体来讲,就是把Await后面要执行的代码放到一个类似ContinueWith的函数中,在C#中,它是以状态机的形式表现的,每个状态都对应一部分代码,状态机有一个MoveNext()方法,MoveNext()根据不同的状态执行不同的代码,然后每个状态部分对应的代码都会设置下一个状态字段,然后把自身的MoveNext()方法放到类似ContinueWith()的函数中去执行,整个状态机由回调函数推动。我们尝试手动转换以下async/await方法。
public static Task WorkAsync()
{
return Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Done!");
});
}
public static async void Test()
{
Console.WriteLine("步骤1");
await WorkAsync();
Console.WriteLine("步骤2");
await WorkAsync();
Console.WriteLine("步骤3");
}
手动写一个简单的状态机类
public class TestAsyncStateMachine
{
public int _state = 0;
public void Start() => MoveNext();
public void MoveNext()
{
switch(_state)
{
case 0:
{
goto Step0;
}
case 1:
{
goto Step1;
}
default:
{
Console.WriteLine("步骤3");
return;
}
}
Step0:
{
Console.WriteLine("步骤1");
_state = 1;
WorkAsync().ContinueWith(t => this.MoveNext());
return;
}
Step1:
{
_state = -1;
Console.WriteLine("步骤2");
WorkAsync().ContinueWith(t => this.MoveNext());
return;
}
}
}
而Test()方法则变成了这样
public static void Test()
{
new TestAsyncStateMachine().Start();
}
注意Test()方法返回的是void,这意味这调用方将不能await Test()。如果返回Task,这个状态机类是不能正确处理的,如果要正确处理,那么状态机在Start()启动后,必须返回一个Task,而这个Task在整个状态机流转完毕后要变成完成状态,以便调用方在该Task上调用的ContinueWith得以继续执行,而就Task这个类而言,它是没有提供这种方法来主动控制Task的状态的,这个与JS中的Promise不同,JS里面用Reslove函数来主动控制Promise的状态,并导致在该Promise上面的Then链式调用得以继续完成,而在C#里面怎么做呢?既然使用了状态机来实现async/await,那么在转换一个返回Task的函数时肯定会遇到,怎么处理?后面讲。
首先解决一下与Task类型紧密联系这个问题。
从状态机中可以看到,主要使用到了Task中的ContinueWith这个函数,它的语义是在任务完成后,执行回调函数,通过回调函数拿到结果,这个编程风格也叫做CPS(Continuation-Passing-Style, 续体传递风格),那么我们能不能把这个函数给抽象出来呢?语言开发者当然想到了,它被抽象成了一个Awaiter因此编译器要求await的类型必须要有GetAwaiter方法,什么样的类型才是Awaiter呢?编译器规定主要实现了如下几个方法的类型就是Awaiter:
- 必须继承INotifyCompletion接口,并实现其中的OnCompleted(Action continuation)方法
- 必须包含IsCompleted属性
- 必须包含GetResult()方法
第一点好理解,第二点的作用是热路径优化,第三点以后讲。我们再改造一下我们手动写的状态机。
public class TestAsyncStateMachine
{
public int _state = 0;
public void Start() => MoveNext();
public void MoveNext()
{
switch(_state)
{
case 0:
{
goto Step0;
}
case 1:
{
goto Step1;
}
default:
{
Console.WriteLine("步骤3");
return;
}
}
Step0:
{
Console.WriteLine("步骤1");
_state = 1;
TaskAwaiter taskAwaiter;
taskAwaiter = WorkAsync().GetAwaiter();
if (taskAwaiter.IsCompleted) goto Step1;
taskAwaiter.OnCompleted(() => this.MoveNext());
return;
}
Step1:
{
_state = -1;
Console.WriteLine("步骤2");
TaskAwaiter taskAwaiter;
taskAwaiter = WorkAsync().GetAwaiter();
if (taskAwaiter.IsCompleted) MoveNext();
taskAwaiter.OnCompleted(() => this.MoveNext());
return;
}
}
}
可以看到去掉了与Task中ContinueWith的耦合关系,并且如果任务已经完成,则可以直接执行下个任务,避免了无用的开销。
因此我们可以总结一下async/await:
- async/await只是表示这个方法需要编译器进行特殊处理,并不代表它本身一定是异步的。
- Task类中的GetAwaiter主要是给编译器用的。
第一点我们可以用以下例子来证明,有兴趣的朋友可以自己去验证以下,以便加深理解。
//该类型包含GetAwaiter方法,且GetAwaiter()返回的类型包含三个必要条件
public class MyAwaiter : INotifyCompletion
{
public void OnCompleted(Action continuation)
{
continuation();
}
public bool IsCompleted { get; }
public void GetResult()
{
}
public MyAwaiter GetAwaiter() => new MyAwaiter();
}
一个测试函数,注意必须返回void
public static async void AwaiterTest()
{
await new MyAwaiter();
Console.WriteLine("Done");
}
可以看到这是完全同步进行的。
觉得有收获的不妨点个赞,有支持才有动力写出更好的文章。
C#异步编程由浅入深(二)Async/Await的作用.的更多相关文章
- 走进异步编程的世界--async/await项目使用实战
起因:今天要做一个定时器任务:五分钟查询一次数据库发现超时未支付的订单数据将其状态改为已经关闭(数据量大约100条的情况) 开始未使用异步: public void SelfCloseGpPayOrd ...
- 异步编程新方式async/await
一.前言 实际上对async/await并不是很陌生,早在阮大大的ES6教程里面就接触到了,但是一直处于理解并不熟练使用的状态,于是决定重新学习并且总结一下,写了这篇博文.如果文中有错误的地方还请各位 ...
- 【C# TAP 异步编程】二 、await运算符已经可等待类型Awaitable
await的作用: 1.await是一个标记,告诉编译器生成一个等待器来等待可等待类型实例的运行结果. 2.一个await对应一个等待器 ,任务的等待器类型是TaskAwaiter/TaskAwait ...
- ES7前端异步玩法:async/await理解 js原生API妙用(一)
ES7前端异步玩法:async/await理解 在最新的ES7(ES2017)中提出的前端异步特性:async.await. 什么是async.await? async顾名思义是“异步”的意思,a ...
- Dart 异步编程(二):async/await
对每一个异步任务返回的 Future 对象都使用链式操作-- then ,超过三个以上时,导致"倒三角"现象,降低代码的可阅读性. getHobbies() { post('htt ...
- C#基础系列——异步编程初探:async和await
前言:前面有篇从应用层面上面介绍了下多线程的几种用法,有博友就说到了async, await等新语法.确实,没有异步的多线程是单调的.乏味的,async和await是出现在C#5.0之后,它的出现给了 ...
- 【转】剖析异步编程语法糖: async和await
一.难以被接受的async 自从C#5.0,语法糖大家庭又加入了两位新成员: async和await. 然而从我知道这两个家伙之后的很长一段时间,我甚至都没搞明白应该怎么使用它们,这种全新的异步编程模 ...
- [C#]剖析异步编程语法糖: async和await
一.难以被接受的async 自从C#5.0,语法糖大家庭又加入了两位新成员: async和await. 然而从我知道这两个家伙之后的很长一段时间,我甚至都没搞明白应该怎么使用它们,这种全新的异步编程模 ...
- 【异步编程】Part1:await&async语法糖让异步编程如鱼得水
前导 Asynchronous programming Model(APM)异步编程模型以BeginMethod(...) 和 EndMethod(...)结对出现. IAsyncResult Beg ...
随机推荐
- css border-radius & yin-yang & taiji
css border-radius & yin-yang & taiji solution css border-radius & tabs effect https://co ...
- 微信小程序 components
微信小程序 components wx-xcx-components https://developers.weixin.qq.com/miniprogram/dev/component/ https ...
- MySQL切换版本踩坑记录(包括恢复数据方法)
踩坑起因:在创建数据库时, 字段:create_time datetime DEFAULT CURRENT_TIMESTAMP, 报异常--Error Code: 1067 - Invalid def ...
- Dockerfile多阶段构建原理和使用场景
本文转载自Dockerfile多阶段构建原理和使用场景 导语 Docker 17.05版本以后,新增了Dockerfile多阶段构建.所谓多阶段构建,实际上是允许一个Dockerfile 中出现多个 ...
- 死磕以太坊源码分析之EVM动态数据类型
死磕以太坊源码分析之EVM动态数据类型 配合以下代码进行阅读:https://github.com/blockchainGuide/ 写文不易,给个小关注,有什么问题可以指出,便于大家交流学习. So ...
- html5的标签中,哪些是行内元素,哪些是块级元素。
块级元素:块级大多为结构性标记 <address>...</adderss> <center>...</center> 地址文字 <h1> ...
- 在js中如何将字符串类型的日期("2020-11-30T02:21:42.000+0000")进行格式化
1.引入方法 import { formatDateNew } from '@/utils' 2.在方法中使用,注意要先将字符串进行new Date(),否则报错date.getFullYear is ...
- Lambad表达式--Java8新特性
1.概述 Lambda是一个匿名函数,是java8的一个新特性.可以对接口进行非常简洁的实现.但它要求接口中只能有一个抽象方法,原因是lambda只能实现一个方法.另外,需要在接口上添加注解@Func ...
- deepin 340 USB转console线驱动安装及使用
刚换DEEPIN系统, 有个路由器要做调整,的确是没windows友好,查了网上资料,归总一下. zhaodong@zhaodong-PC:sudo find / -name serial 进入 zh ...
- nacos服务注册与发现之客户端
服务注册 1.1 NamingService.registerInstance的方法为客户端提供的服务注册接口 1.2 客户端通过调用NamingService.registerService上报到n ...