iT邦幫忙

1

python的asyncio模組(七):Future對象與Task對象(三)

前言

這篇文章需要對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又有了很大的進展,因為上一篇文章的層數不太夠,可讀性無法優化太多,這次我們再舉一個新的例子:

https://ithelp.ithome.com.tw/upload/images/20200530/20107274pIEypmyEJb.png

今天我想要呼叫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.04

import asyncio

def 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.04

import asyncio

loop = 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的實現做了詳細的比對,主要是想讓讀者體會到異步程式在程式結構上遇到的難題與改進方案。

下一篇終於又能回到基本語法的探討了(灑花~~


尚未有邦友留言

立即登入留言