淺析pplx庫的設計與實現。

bbqz007發表於2020-06-19

主要有三部分組成,threadpool,scheduler,task。

 

 三者關係如上圖示,pplx只著重實現了task部分功能,scheduler跟threadpool只是簡略實現。

 threadpool主要依賴boost.asio達到跨平臺的目標,cpprestsdk的 io操作同時也依賴這個threadpool。

pplx提供了兩個版本的scheduler,分別是

linux_scheduler依賴boost.asio.threadpool。

window_schedule依賴win32 ThreadPool。

預設的scheduler只是簡單地將work投遞到threadpool進行分派。

使用者可以根據自己需要,實現scheduler_interface,提供複雜的排程。

 

每個task關聯著一個_Task_impl實現體,一個_TaskCollection_t(喚醒事件,後繼任務佇列,這個佇列的任務之間的關係是並列的),還有一個_PPLTaskHandle程式碼執行單元。

task,並行執行的單位任務。通過scheduler將程式碼執行單元排程到執行緒去執行。

task提供類似activeobject模式的功能,可以看作是一個future,通過get()同步阻塞等待執行結果。

task提供拓撲模型,通過then()建立後續task,並作為後繼執行任務。注意的是每個task可以接受不限數量的then(),這些後繼任務之間並不序列。例 task().then().then()序列,(task1.then(), task1.then())並行。一個任務在執行完成時,會將結果傳遞給它的所有直接後繼執行任務。

 

此外,task拓撲除了then()函式外,還可以在執行lambda中新增並行分支,然後可以在後繼任務中同步這些分支。

也就是說後繼任務同步原本task拓撲外的task拓撲才能繼續執行。

 1 auto fork0 =
 2      task([]()->task<void>{
 3         auto fork1 = 
 4             task([]()->task<void>{
 5                 auto fork2 =
 6                     task([](){
 7                             // do your fork2 work
 8 
 9                             });
10                  // do your fork1 work
11 
12                  return fork2;
13              }).then([](task<void>& frk2){ frk2.wait(); });     // will sync fork2
14           // do your fork0 work
15 
16          return fork1;
17       }).then([](task<void>& frk1){ frk1.wait(); });       // will sync fork1
18 fork0.wait();            // sync fork1, fork2

上面的方式有一個問題,如果裡層的fork先完成,將不要阻塞執行緒,但是外層fork先完成就不得不阻塞執行緒等待內層fork完成。

所以可以用when_all

task<task<void> >([]()->task<void> {
    std::vector<task<void> > forks;
    forks.push_back( task([]() { /* do fork0 work */ }) );
    forks.push_back( task([]() { /* do fork1 work */ }) );
    forks.push_back( task([]() { /* do fork2 work */ }) );
    forks.push_back( task([]() { /* do fork3 work */ }) );
    return when_all(std::begin(forks), std::end(forks));
}).then([](task<void> forks){
    forks.wait();
}).wait();

通過上面的方式,也可以在lambda中,將其它task拓撲插入到你原來的task拓撲。

task結束,分兩種情況,完成以及取消。取消執行,只能在執行程式碼時通過丟擲異常,task並沒有提供取消的介面。任務在執行過程中丟擲的異常,就會被task捕捉,並暫存異常,然後取消執行。異常在wait()時重新丟擲。下面的時序分析可以看到全過程 。

 

 

值得注意的是,PPL中task原本的設計是的有Async與Inline之分的。在_Task_impl_base::_Wait()有一小段註釋說明

// If this task was created from a Windows Runtime async operation, do not attempt to inline it. The
// async operation will take place on a thread in the appropriate apartment Simply wait for the completed
// event to be set.
            
                

也就是task除了由scheduler排程到執行緒池分派執行,還可以強制在wait()函式內分派執行,後繼task也不必再次排程而可以在當前執行緒繼續分派執行。但是pplx沒有實現

class _TaskCollectionImpl
{
    ...
    void _Cancel()
    {
        // No cancellation support
    }

    void _RunAndWait()
    {
        // No inlining support yet
        _Wait();
    }

 

 

 下面是對task的時序分析。

開始的task建立_InitialTaskHandle, 一種只能用於始首的Handle執行單元。

 

通過then()新增的task,建立_ContinuationTaskHandle,(一種可以入鏈的後繼執行單元),並暫存起來。

 

當一個任務線上程池中分派結束時,就會將所有通過then()新增到它結尾的後繼任務一次過向scheduler排程出去。

任務只能通過丟擲異常從而自己中止執行,task並暫存異常(及錯誤資訊)。

 

 

 後繼任務被排程到執行緒池繼續分派執行。

 

這裡順便討論一個開銷,在window版本中,每個task都有一個喚醒事件,使用事件核心物件,都要建立釋放一個核心物件,在高並行任務時,可能會消耗過多核心物件,消耗控制程式碼數。

並且continuation後繼任務,在預設scheduler排程下,不會在同一執行緒中分派,所有後繼任務都會簡單投遞到執行緒池。由執行緒池去決定分派的執行緒。所以由then()序列起來的任務可能會由不同的執行緒順序分派,從而產生開銷。因為pplx並沒有實現 Inline功能,所有task都會視作Async重新排程到執行緒池。

 

相關文章