學習 Rust Futures - Executor and Task

在最開始學習 Rust futures 的時候,executor 和 task 是兩個讓我比較困惑的概念,這兩個東西到底是啥,它們到底是如何使用的,我當時完全不清楚。等後來做完一些項目,才慢慢理解了。所以覺得有必要好好的記錄一下。

介紹

Executor 可以認爲是一個用來執行 future 的地方,我們可以在當前線程裏面執行 future,也可以將 future 扔到一個 thread pool 裏面去執行,也可以在 event loop 裏面(例如 tokio-core)裏面去執行。

而 Task 則可以認爲是一種正在或者將會被執行的 future。通常,我們會將多個 future 組合成一個大的工作單元,然後會在 executor 上面 spawn 一個對應的 task。Executor 會負責當通知到來的時候,去 poll future,直到 future 全被執行結束。

整個流程可以簡化爲:

  1. 當一個 future 不是 ready 的時候,我們使用 task::current() 函數得到一個 task handle,並 block 住當前的 future。
  2. 將 task handle 加入到一個感興趣的事件隊列裏面,如果相關事件觸發了,則通過 task.notify() 通知對應的 executor。
  3. Executor 繼續 poll future。

上面可能比較抽象,我們可以通過幾個例子更深刻的瞭解相關的機制。

Future wait

當我們創建了一個 future 之後,可以使用 wait 函數,block 住當前的線程,強制等到 future 被執行,然後纔會繼續進行後面的操作。

Future wait 函數的實現如下:

fn wait(self) -> result::Result<Self::Item, Self::Error>
    where Self: Sized
{
    ::executor::spawn(self).wait_future()
}

可以看到,我們使用 executor::spawn 了一個 Spawn 對象,Spawn 對象表示的是一個 fused future 和 task, 這就意味着我們不能再將 future 跟其他的 future 去組合了,只能執行了。

wait_future 函數裏面,我們會 block 住當前的線程,直到 Spawn 內部的 future 執行完畢,代碼如下:

pub fn wait_future(&mut self) -> Result<F::Item, F::Error> {
    let unpark = Arc::new(ThreadUnpark::new(thread::current()));

    loop {
        match self.poll_future_notify(&unpark, 0)? {
            Async::NotReady => unpark.park(),
            Async::Ready(e) => return Ok(e),
        }
    }
}

首先我們會創建一個 ThreadUnpark 的 Notify 對象,然後傳給 Spawn 的 poll_future_notify 去使用。當 future 變成 ready 的時候,我們會去調用 Notify 的 notify 函數去通知相關的 executor 繼續去 poll 這個 future。

在 ThreadUnpark 裏面,notify 實現如下:

impl Notify for ThreadUnpark {
    fn notify(&self, _unpark_id: usize) {
        self.ready.store(true, Ordering::SeqCst);
        self.thread.unpark()
    }
}

notify 函數裏面,我們直接會調用 thread 的 unpark 函數,用來喚醒當前被 block 的線程。

Spawn 的 poll_future_notify 會嘗試 poll 內部的 future,這個函數會接受一個 NotifyHandle 參數,後續任何的 task::current() 操作返回的 task handle 都會帶上這個 NotifyHandle,這樣我們通過 task.notify() 就能告訴 executor future 已經 ready 了。

如果 poll_future_notify 返回 NotReady,我們就需要靠 Notify 來通知了。在上面的例子中,返回 NotReady 之後,我們直接調用了 pack 函數,定義如下:

fn park(&self) {
    if !self.ready.swap(false, Ordering::SeqCst) {
        thread::park();
    }
}

park 裏面,我們直接調用了 thread 的 park 函數,block 住了當前線程,這樣當 future 已經 ready 之後,我們會調用 thread 的 unpark 函數喚醒被 block 的線程。

gRPC

上面是一個簡單使用操作系統 thread 的 park/unpark 函數來處說明 Executor 和 Task 的例子,在 rust gRPC 裏面,我們爲了跟 gRPC 的 event loop 整合,也實現了相關的操作。

這裏先介紹一下 gRPC 的相關概念,在 gRPC 裏面,所有的事件都是通過 CompletionQueue ( 後面以 CQ 代替) 來驅動的,我們會不停的循環調用 CQ 的 next 函數,當有事件產生的時候,next 就會返回對應的事件,然後我們會通過這個事件裏面的 tag 找到對應的上下文繼續處理。

通常我們都是在 for 循環裏面調用的 next 函數,其它線程如果想跟 CQ 發送消息,就需要通過 gRPC 裏面的 alarm 機制,我們會先通過 grpc_alarm_create 創建一個 alarm,然後調用 grpc_alarm_cancel 就可以直接去通知到 CQ 了。當 next 返回對應的 alarm event 之後,我們就可以執行這個 alarm 相關的邏輯了。

當 CQ 線程調用到對應的 gRPC method 之後,我們可能需要在其他線程去處理相關的操作,這時候,就可以通過 executor::spawn 來生成一個 Spawn,代碼如下:

pub struct Executor<'a> {
    cq: &'a CompletionQueue,
}

impl<'a> Executor<'a> {
    pub fn spawn<F>(&self, f: F)
    where
        F: Future<Item = (), Error = ()> + Send + 'static,
    {
        let s = executor::spawn(Box::new(f) as BoxFuture<_, _>);
        let notify = Arc::new(SpawnNotify::new(s, self.cq.clone()));
        poll(notify, false)
    }
}

SpawnNotify 對應的就是一個 Notify 對象,SpawnNotify 會創建一個 SpawnHandle,在對應的 notify 函數裏面,我們會調用 SpawnHandle 的 notify 函數,這個函數裏面就會創建一個 alarm 並通知 CQ。

    pub fn notify(&mut self, tag: Box<CallTag>) {
        self.alarm.take();
        let mut alarm = Alarm::new(&self.cq, tag);
        alarm.alarm();
        // We need to keep the alarm until tag is resolved.
        self.alarm = Some(alarm);
    }

當 CQ 的 next 返回了對應的 alarm 事件之後,我們會調用到 SpawnNotify 的 resolve 函數:

    pub fn resolve(self, success: bool) {
        // it should always be canceled for now.
        assert!(!success);
        poll(Arc::new(self.clone()), true);
    }

最後我們在關注下 poll 函數,無論是 Executor 的 spawn 還是SpawnNotify 的 resolve 裏面,我們最後都會使用。poll 會調用 Spawn 的 poll_future_notify 函數:

fn poll(notify: Arc<SpawnNotify>, woken: bool) {
    let mut handle = notify.handle.lock();
    ......
    match handle.f.as_mut().unwrap().poll_future_notify(&notify, 0) {
        Err(_) | Ok(Async::Ready(_)) => {
            ......
            return;
        }
        _ => {}
    }
}

poll_future_notify 如果返回 NotReady,這裏我們並不需要做特殊的處理,因爲 CQ 會不停的調用 next,如果沒有任何事件產生,next 自動回 block 住當前的 CQ 的進程,如果 future 變成了 ready,我們就可以告訴 CQ,CQ 自然會在 next 裏面得到對應的事件,然後我們就能繼續去執行這個 future 了。

 



作者:siddontang
鏈接:https://www.jianshu.com/p/feafe6346929
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章