上面有兩篇文章介紹了兩類比較典型且簡單的並行方法,並且也簡單地介紹了它們的效能以及優缺點。這裡將再一種介紹方法:通過微線程來同步多個相互協作的並行任務。
我們來想像一下下面這個問題:在一個已知長度的巨大線性表(如一維數組)當中要搜尋一個元素,並且其中的元素都是唯一的。我們現在所用的處理器有兩個核,根據前面介紹的第二種並行方法,我們可以簡單地讓核A去搜尋前一半元素,讓核B去搜尋後一半元素。當時我們將搜尋操作作為單獨的一個線程進行處理。但是像搜尋這樣的操作我們往往會在它結束後立即獲得結果,然後接下去做其他事情。這時候,這個搜尋操作將成為某個線程(任務)的一個串列(順序操作)部分。而且這也顯得比較自然。不過這也帶來了一個問題:當一個核找到結果時如何讓另一個核停止搜尋動作而馬上處理後面的事情。我這裡想引入一個“微線程”的概念來更有效地處理這種情況。
我這裡的“微線程”指的是:依附於線程,其棧空間為其所依附的線程的棧空間的子區間,並且微線程的操作作為線程的作業流的一部分,只有當微線程的操作完成後,才執行線程的其餘部分。
可以用以下模型表示:
int ThreadA_Handler(void){ Initialize(); while(do_event(event_id1)) { void* mem = AllocMemPool(MEM_SIZE_1024B); Initialize_Resource(mem); // Create micro-thread int elem = StartMicroThread( /*thread id*/task_id_threadA, /*micro-thread handler*/&do_handlerMA, /*the argument for the handler*/mem, ); // Process the result Output(elem); } return THREAD_SUCCESS;}
上述代碼中,StartMicroThread()函數就是用來建立微線程的介面。它記錄下當前線程的ID,這個將用於後面將要講述的中斷處理中的微線程指派處理;然後將微線程處理函數傳遞進去;再傳入微線程的輸入參數。
下面將描述一下StartMicroThread()可能的樣子:
void* StartMicroThread(TASK_ID task_id, /*input*/ void*(*pMicroThreadHandler)(void*),/*input*/ void* pParam, /*input*/ ){ unsigned int context = sys_fetch_ret_address(); RegisterMicroThreadHandler(task_id, context); void* ret = (*pMicroThreadHandler)(pParam); return ret;}
上述代碼中,sys_fetch_ret_address()系統函數表示擷取當前函數所要返回的地址,這樣就可以利用外來事件直接取消微線程的操作而轉到StartMicroThread()的函數調用的下一條指令地址,當然這裡可以加上一點處理來回收被使用的棧空間。可以在StartMicroThread()函數調用前後增加內容相關的保護與恢複(這裡的上下文保護與恢複無非就是對棧指標和幀指標(在x86中稱為基指標BP)的保護與恢複)。
RegisterMicroThreadHandler()用來註冊線程ID以及返回地址。當一個核通知另一個核動作已完成,使得另一個核立即結束微線程操作時,在中斷處理中,指派器將判斷當前活動線程是否為登入的線程,若是,則將中斷返回地址設為context值;否則,為該線程設定標誌位,並且將context值傳遞給它,使得當該線程被再次調度時能立即結束微線程操作。因此,這個微線程概念也基於調度預先處理機制,這個機制在目前很多主流作業系統中並沒有使用。
第三條語句就是調用使用者自訂的微線程處理函數,然後將結果返回。這裡,往往可能將結果返回到某個共用儲存區的變數中,因為很有可能在微線程操作沒有完全操作完成時被強制退出,這時,傳回值是不定的。所以這裡設定傳回值可能並不是一個好主意。
下面將利用宏函數來加入對棧內容相關的保護與恢複:
#define BEGIN_MICRO_THREAD(task_id, handler, param) { / SAVE_STACK_CONTEXT(); / StartMicroThread(task_id, handler, param); / RESTORE_STACK_CONTEXT(); /}
對於SAVE_STACK_CONTEXT()和RESTORE_STACK_CONTEXT()可以根據具體系統來實現。當然,使用內嵌彙編的形式也完全沒有問題。
然後,我們有時可能會在微線程操作中申請了儲存資源、鎖或其它等資源。在這種情況下我們也可以利用一些手段來處理由於被強迫終止操作而沒有被釋放的資源。這個問題與線程中資源申請及釋放的操作差不多,可以進行參考。像C++中的try-catch機制也值得參考。
最後還有一個問題不能被忽略。也就是退出微線程處理函數後應當登出所註冊的上下文。我們可以再定義一個宏函數——
#define END_MICRO_THREAD(task_id) { / UnregisterMicroThreadHandler(task_id); / // You may add some resource releasing function calls here /}
這樣在使用時,兩個宏函數正好成對使用。
下面我們來討論一下微線程處理函數裡面操作的一些更具體的問題。
還是以搜尋操作為例。在這個機制下我們基本上需要確定獲得結果的微線程所附屬的線程所在的核。就這點而言,微線程機制更適合於嵌入式系統。而另一個微線程作為計算的輔助操作。一般,總是輔助操作向獲得結果線程所處的核發送插斷要求訊號,當它完成操作並且獲得結果的微線程沒有完成操作時。而獲得結果的微線程當它完成操作後,但沒有獲得操作結果時(比如沒有所搜到指定資料),那麼就得等待另一個核的操作完成。因此,這個微線程只有當獲得合適的結果時才會真正結束其操作,然後線程繼續執行。而這也是為什麼註冊與登出內容相關的函數只需要一個線程ID而不需要其它標誌的原因。
作為輔助計算的微線程可以被調度到與獲得結果的微線程所處的同一個核中。在這種情況下,效率也不會被降低,尤其是在另一個核總是忙於處理其它事務而長時間無法調度到這個搜尋微線程所屬的線程的時候。
最後,對於微線程處理函數的實現也可以參考:http://download.csdn.net/source/297300