前言
这篇文章需要对javascript的promise和async/await有基本的认识,对不熟的读者可能不太友善,需要自行google,请大家海涵orz
python的asyncio模组(六):Future对象与Task对象(二)解释了Future物件如何实现异步程式,并和javascript的promise物件的实现做了详细的语法对应,也让我们能比较好的了解其概念的共通性。
这一次的教学会用新的情境,来好好的解释javascript从callback到promise再到async/await,是如何简化程式的结构,并再引入python的Future对象和Task对象来实作新的情境。
最后我们会得到的结论是,Task对象的实作和async/await的实作有着近乎相同的结构,两者对异步程式的实现都有目前最简洁的结构,这就是为什么我们不单满足于只用Future物件来实作异步程式,而要再引入Coroutine和Task对象。
Task对象与Coroutine的使用
Javascript在异步执行的可读性上,从ES6的Promise到ES7的async/await又有了很大的进展,因为上一篇文章的层数不太够,可读性无法优化太多,这次我们再举一个新的例子:
今天我想要呼叫Func_A最后得到Func_D的数值,若是用一般的callback来实作就像这样:
let Func_A = (cb) => { console.log("Start exec Func_A"); setTimeout(()=> { cb('Value_A'); }, 1000);}let Func_B = (value, cb) => { console.log("Start exec Func_B"); console.log("Func_B get value: " + value); setTimeout(()=> { cb('Value_B'); }, 1000);}let Func_C = (value, cb) => { console.log("Start exec Func_C"); console.log("Func_C get value: " + value); setTimeout(()=> { cb('Value_C'); }, 1000);}let Func_D = (value) => { console.log("Start exec Func_D"); console.log("Func_D get value: " + value); setTimeout(()=> { console.log("Final result: Value_D"); }, 1000);}Func_A(function(value_a) { Func_B(value_a, function(value_b) { Func_C(value_b, function(value_c) { Func_D(value_c); }); });});
当我们一把Func_A到Func_D串接起来,免不了会产生金字塔状的callback hell。
因为在步骤之间我们还要处理数值的传递,像是Func_A的cb参数必须塞入一个function,其接收了value_a并在内部呼叫Func_B并塞入参数value_a,每一执行这种行为便会增长一层巢状结构,当层数太多就会大大降低程式可读性,甚至会不容易看出步骤之间的顺序与关係。
原本只是照顺序传递数值,很平面的程式逻辑,却必须用层层叠叠的巢状结构来实现,怎么想都很让人崩溃XD
如果是用promise来执行:
let Func_A = () => { return new Promise((resolve, reject) => { console.log("Start exec Func_A"); setTimeout(()=> { resolve('Value_A'); }, 1000); });}let Func_B = (value) => { return new Promise((resolve, reject) => { console.log("Start exec Func_B"); console.log("Func_B get value: " + value); setTimeout(()=> { resolve('Value_B'); }, 1000); });}let Func_C = (value) => { return new Promise((resolve, reject) => { console.log("Start exec Func_C"); console.log("Func_C get value: " + value); setTimeout(()=> { resolve('Value_C'); }, 1000); });}let Func_D = (value) => { return new Promise((resolve, reject) => { console.log("Start exec Func_D"); console.log("Func_D get value: " + value); setTimeout(()=> { console.log("Final result: Value_D"); }, 1000); });}Func_A().then(Func_B).then(Func_C).then(Func_D);
用promise改写后的程式大大改善了程式的可读性,不但消除了巢状结构,每个函数也不需要特别去指定一个cb参数,且最后用.then()组成的串链,很明确的表达了每一个步骤所在的位置。
如果是用async/await来执行:
let sleep = (ms) => { return new Promise(resolve => setTimeout(resolve, ms));}let Func_A = async () => { console.log("Start exec Func_A"); await sleep(1000); return 'Value_A';}let Func_B = async (value) => { console.log("Start exec Func_B"); console.log("Func_B get value: " + value); await sleep(1000); return 'Value_B';}let Func_C = async (value) => { console.log("Start exec Func_C"); console.log("Func_C get value: " + value); await sleep(1000); return 'Value_C';}let Func_D = async (value) => { console.log("Start exec Func_D"); console.log("Func_D get value: " + value); await sleep(1000); console.log("Final result: Value_D");}(async () => { let value_a = await Func_A(); let value_b = await Func_B(value_a); let value_c = await Func_C(value_b); await Func_D(value_c);})();
首先async/await对setTimeout巢状结构做了改善,把setTimeout用promise包起来,并独立成为一个sleep func,然后用await关键字等待sleep promise的完成。
async/await的优点是,他可以消除promise.then()的巢状结构,原本只能塞在Func_A.then()里面的Func_B,现在可以直接移到Func_A的下一行去执行,让原有的程式逻辑在实现上也能完全的平面化(没有任何的巢状结构)。
而且.then()还有一个缺点,就是各个.then()之间的变数不能够共享,比如说.then(Func_B)的接收参数value_a并不在.then(Func_D)的作用域里,假设我们要调整一下程式逻辑,让Func_D必须接收参数value_a,那我们的结构必须调成以下形式:
// 现在Func_D必须从Func_C和Func_A接收参数let Func_D = (value, value_a) => { return new Promise((resolve, reject) => { console.log("Start exec Func_D"); console.log("Func_D get value: " + value); console.log("get value from Func_A: " + value_a); setTimeout(()=> { console.log("Final result: Value_D"); }, 1000); });}let value_a_backup; // 设立一个global variable储存value_a给Func_D使用Func_A().then(function(value_a) { value_a_backup = value_a; return Func_B(value_a);}).then(Func_C).then(function(value_c) { return Func_D(value_c, value_a_backup);});
我们必须特别设置一个作用域更广的变数来储存value_a,之后才能给Func_D使用。
但如果我们使用async/await来实现:
let Func_D = async (value, value_a) => { console.log("Start exec Func_D"); console.log("Func_D get value: " + value); console.log("get value from Func_A: " + value_a); await sleep(1000); console.log("Final result: Value_D");}(async () => { let value_a = await Func_A(); let value_b = await Func_B(value_a); let value_c = await Func_C(value_b); await Func_D(value_c, value_a);})();
因为所有流程都是平面化,所以作用域也共享,那直接把value_a塞进Func_D就可以了,这个机制让程式可以更自由的根据需求而变动,也就是说可维护性更高。
另外async/await还有一个好处是我们不需要再显式的定义出一个promise物件再把他return出去,因为整个async function的架构就是奠基于promise物件之上。
但追溯到我们等待的源头,也就是程式的timer,我们还是要明确的定义出setTimeout的内容以及其外层包裹的promise物件,async/await终究是一个让程式变得简洁的语法糖,在程式中不能让这语法糖所依赖的基础语法完全再程式中消失。
async/await唯一一个有点小麻烦的地方是若要使用await关键字,那必须要在async function内,也就是这个function在定义时必须使用到async关键字,这也就是为什么我们使用await时,必须被包在(async () => {})();裏面。
接下来我们可以检验若asyncio的Future和Task来实作上述流程,会不会也能有类似的可读性的提升。
使用Future对象撰写:
# python3.5# ubuntu 16.04import asynciodef Func_A(): print("Start exec Func_A") def Func_A_setTimeout(): future_A.set_result('Value_A') future_A._loop.call_later(1, Func_A_setTimeout)def Func_B(future): print("Start exec Func_B") value = future.result() print("Func_B get value: " + value) def Func_B_setTimeout(): future_B.set_result('Value_B') future_B._loop.call_later(1, Func_B_setTimeout)def Func_C(future): print("Start exec Func_C") value = future.result() print("Func_C get value: " + value) def Func_C_setTimeout(): future_C.set_result('Value_C') future_C._loop.call_later(1, Func_C_setTimeout)def Func_D(future): print("Start exec Func_D") value = future.result() print("Func_D get value: " + value) def Func_D_setTimeout(): print("Final result: Value_D") future_D.set_result(None) future._loop.call_later(1, Func_D_setTimeout)loop = asyncio.get_event_loop()future_A = loop.create_future()future_A.add_done_callback(Func_B)future_B = loop.create_future()future_B.add_done_callback(Func_C)future_C = loop.create_future()future_C.add_done_callback(Func_D)future_D = loop.create_future()loop.call_soon(Func_A)loop.run_until_complete(asyncio.wait([future_A, future_B, future_C, future_D]))
因为python的asyncio模组(六):Future对象与Task对象(二)有提到asyncio中的Future相当于javascript的promise,所以我们需要定义出4个Future物件,以取代javascript promise程式中4个程式所return出来的promise物件。
然后依照.then()所串接的任务顺序,我们让每个future物件加入各自对应的callback,也就是add_done_callback在做的事情。
这个Future程式和promise程式有着一样的缺点,就是每一个Func所使用的参数不能够共享,而这在下面的Task程式能够得到很好的解法,可读性也有所提升。
使用Task对象撰写:
# python3.5# ubuntu 16.04import asyncioloop = asyncio.get_event_loop()async def Func_A(): print("Start exec Func_A") await asyncio.sleep(1) return 'Value_A'async def Func_B(value): print("Start exec Func_B") print("Func_B get value: " + value) await asyncio.sleep(1) return 'Value_B'async def Func_C(value): print("Start exec Func_C") print("Func_C get value: " + value) await asyncio.sleep(1) return 'Value_C'async def Func_D(value): print("Start exec Func_D") print("Func_D get value: " + value) await asyncio.sleep(1) print("Final result: Value_D")async def main(): value_a = await Func_A() value_b = await Func_B(value_a) value_c = await Func_C(value_b) await Func_D(value_c)loop.run_until_complete(main())
改成Task对象的写法后,我们发现这跟async/await程式的写法结构几乎一样,每个语法彼此都有完美的对应!
另外说明一下asyncio.sleep()的实作,因为底层的概念有些複杂,必须要在以后会开的asyncio源码解析的系列文章才会详细解说,但大概的内容是在其内部会用loop.create_future()创建一个future,然后用future._loop.call_later等待一个timer,最后会执行future.set_result()。
然后因为Coroutine必须用Task对象封装起来并执行,所以执行中的Task对象对内会侦测到这一个future物件,并暂停整个Coroutine内容的执行,一直到future.set_result()被执行完毕,才会继续运行Coroutine。
结尾
我们花了三篇的篇幅探讨了Future对象和Task对象的意义,和两者实现异步程式所显现出来的结构,其中又和javascript的实现做了详细的比对,主要是想让读者体会到异步程式在程式结构上遇到的难题与改进方案。
下一篇终于又能回到基本语法的探讨了(洒花~~