這是一個用於啟用基於任務的多執行緒的函式庫。它允許執行具有任意依賴關係的任務圖。依賴關係被表示為原子計數器。
在幕後,任務圖是使用纖程執行的,而纖程又在工作執行緒池上執行(每個 CPU 核心一個執行緒)。這允許調度程序等待依賴項,而無需任務鍊或上下文切換。
該庫最初是作為 Christian Gyrling 在 2015 年 GDC 演講“使用光纖並行化頑皮狗引擎”中提出的想法的概念證明而創建的
免費 GDC Vault 示範錄音
幻燈片
# include " ftl/task_scheduler.h "
# include " ftl/wait_group.h "
# include < assert.h >
# include < stdint.h >
struct NumberSubset {
uint64_t start;
uint64_t end;
uint64_t total;
};
void AddNumberSubset (ftl::TaskScheduler *taskScheduler, void *arg) {
( void )taskScheduler;
NumberSubset *subset = reinterpret_cast <NumberSubset *>(arg);
subset-> total = 0 ;
while (subset-> start != subset-> end ) {
subset-> total += subset-> start ;
++subset-> start ;
}
subset-> total += subset-> end ;
}
/* *
* Calculates the value of a triangle number by dividing the additions up into tasks
*
* A triangle number is defined as:
* Tn = 1 + 2 + 3 + ... + n
*
* The code is checked against the numerical solution which is:
* Tn = n * (n + 1) / 2
*/
int main () {
// Create the task scheduler and bind the main thread to it
ftl::TaskScheduler taskScheduler;
taskScheduler. Init ();
// Define the constants to test
constexpr uint64_t triangleNum = 47593243ULL ;
constexpr uint64_t numAdditionsPerTask = 10000ULL ;
constexpr uint64_t numTasks = (triangleNum + numAdditionsPerTask - 1ULL ) / numAdditionsPerTask;
// Create the tasks
// FTL allows you to create Tasks on the stack.
// However, in this case, that would cause a stack overflow
ftl::Task *tasks = new ftl::Task[numTasks];
NumberSubset *subsets = new NumberSubset[numTasks];
uint64_t nextNumber = 1ULL ;
for ( uint64_t i = 0ULL ; i < numTasks; ++i) {
NumberSubset *subset = &subsets[i];
subset-> start = nextNumber;
subset-> end = nextNumber + numAdditionsPerTask - 1ULL ;
if (subset-> end > triangleNum) {
subset-> end = triangleNum;
}
tasks[i] = { AddNumberSubset, subset };
nextNumber = subset-> end + 1 ;
}
// Schedule the tasks
ftl::WaitGroup wg (&taskScheduler);
taskScheduler. AddTasks (numTasks, tasks, ftl::TaskPriority::Normal, &wg);
// FTL creates its own copies of the tasks, so we can safely delete the memory
delete[] tasks;
// Wait for the tasks to complete
wg. Wait ();
// Add the results
uint64_t result = 0ULL ;
for ( uint64_t i = 0 ; i < numTasks; ++i) {
result += subsets[i]. total ;
}
// Test
assert (triangleNum * (triangleNum + 1ULL ) / 2ULL == result);
( void )result;
// Cleanup
delete[] subsets;
// The destructor of TaskScheduler will shut down all the worker threads
// and unbind the main thread
return 0 ;
}
對於纖維和整體概念的精彩介紹,我建議觀看 Christian Gyrling 的演講。 (截至撰寫本文時)可以從 GDC 庫免費觀看。也就是說,我將在下面概述該庫的工作原理:
光纖由堆疊和暫存器的小儲存空間組成。它是一個在執行緒內運行的非常輕量級的執行上下文。您可以將其視為實際線程的外殼。
纖維的美妙之處在於您可以非常快速地在它們之間切換。最終,切換包括保存暫存器,然後交換執行指標和堆疊指標。這比完整的線程上下文切換要快得多。
為了回答這個問題,讓我們與另一個基於任務的多執行緒庫進行比較:英特爾的執行緒建構模組。 TBB 是一個極其完善且成功的任務庫。它可以處理非常複雜的任務圖,並具有出色的調度程序。然而,讓我們想像一個場景:
任務A建立任務B、C和D並將它們傳送到調度程序
任務 A 做了一些其他工作,但隨後它遇到了依賴關係:B、C 和 D 必須完成。
如果他們還沒完成,我們可以做兩件事:
自旋等待/睡眠
向調度程序請求新任務並開始執行該任務
我們走第二條路
所以調度程式給我們任務G,我們開始執行
但任務 G 最終也需要依賴項,因此我們向調度程序請求另一個新任務
還有另一個,還有另一個
同時,任務B、C、D已完成
任務A理論上可以繼續,但它被埋在我們等待時得到的任務下面的堆疊中
我們恢復 A 的唯一方法是等待整個鏈恢復到它,或遭受上下文切換。
顯然,這是一個人為的例子。正如我上面所說,TBB 有一個很棒的調度程序,可以努力緩解這個問題。也就是說,光纖可以透過允許任務之間廉價的切換來幫助完全消除這個問題。這使我們能夠將一項任務的執行與另一項任務的執行隔離開來,從而防止上述的「連結」效應。
任務隊列- 用於保存等待執行的任務的「普通」隊列。在目前程式碼中,有一個「高優先權」佇列和一個「低優先權」佇列。
光纖池- 用於在目前任務等待依賴項時切換到新任務的光纖池。纖維執行任務
工作執行緒- 每個邏輯 CPU 核心 1 個。它們運行纖維。
等待任務- 等待依賴關係被滿足的所有纖程/任務。依賴關係用 WaitGroup 表示
任務可以在堆疊上建立。它們只是一個簡單的結構體,帶有一個函數指標和一個可選的 void *arg 來傳遞給函數:
struct Task {
TaskFunction Function;
void *ArgData;
};
Task tasks[ 10 ];
for ( uint i = 0 ; i < 10 ; ++i) {
tasks[i] = {MyFunctionPointer, myFunctionArg};
}
您可以透過呼叫 TaskScheduler::AddTasks() 來安排任務執行
ftl::WaitGroup wg (taskScheduler);
taskScheduler-> AddTasks ( 10 , tasks, ftl::TaskPriority::High, &wg);
任務被添加到隊列中,其他線程(或當前線程,當前任務完成時)可以在它們從隊列中彈出時開始執行它們。
AddTasks 可以選擇使用指向 WaitGroup 的指標。如果這樣做,WaitGroup 的值將按排隊任務的數量遞增。每次任務完成時,WaitGroup 都會自動遞減。您可以使用此功能在任務之間建立依賴關係。你可以用這個函數來做到這一點
void WaitGroup::Wait ();
這就是纖維發揮作用的地方。如果 WaitGroup 的值 == 0,則函數通常會傳回。如果沒有,排程器會將目前的 Fiber 移至 WaitGroup 中的等待 Fiber 清單中,並從Fiber Pool取得新的 Fiber。新的 Fiber 從任務佇列中彈出一個任務並開始執行。
但是我們儲存在 WaitGroup 中的任務/纖程呢?什麼時候執行完畢?
當 WaitGroup 值從減量變為零時,我們將所有等待纖程加回 TaskScheduler 的佇列中。下次執行緒切換纖程時(因為目前纖程完成,或因為它呼叫了 WaitGroup::Wait() ),就緒的任務將被擷取並從中斷處恢復。
一般來說,您不應該在 Fiber 程式碼中使用互斥體,原因有二:
如果您使用互斥體並呼叫 WaitGroup::Wait(),當 Wait() 恢復時,您的程式碼可能位於另一個執行緒上。互斥體解鎖將是未定義的行為,並可能導致死鎖
互斥鎖爭用將阻塞工作執行緒。由於我們通常不會超額訂閱核心的線程,因此這會使核心閒置。
為了解決這個問題,我們創建了 Fibtex。它實現了std 可鎖定接口,因此您可以將它與所有您喜歡的包裝器(std::lock_guard、std::unique_lock 等)一起使用。 ,服務員可以切換到另一項任務並做有價值的工作
當纖程在 WaitGroup::Wait() 或 Fibtex::lock() 之後恢復時,不能保證它將在掛起時運行的相同執行緒上恢復。對於大多數程式碼來說,這很好。然而,某些庫有很強的假設。例如,在 DirectX 中,您必須從建立交換鏈的相同執行緒執行最終訊框提交。因此,某些程式碼需要保證纖程在掛起時運行的相同執行緒上恢復。為此,您可以使用參數pinToCurrentThread
。當設定為true
時,調度程序將保證恢復的纖程將在同一執行緒上執行。此參數可用於 WaitGroup::Wait() 和 Fibtext::lock()。注意:執行緒固定比預設行為更昂貴,並且可能會導致相關任務的恢復速度慢得多(因為它需要固定執行緒來完成目前正在執行的任務)。因此,只有在確實必要時才應該使用它。
C++11 編譯器
CMake 3.2 或更高版本
拱 | 視窗 | Linux | 作業系統 | iOS系統 | 安卓 |
手臂 | 需要測試 | 完全支持 | 理論上 | 理論上 | 理論上 |
手臂_64 | 需要測試 | 完全支持 | 需要測試 | 理論上 | 理論上 |
x86 | 完全支持 | 需要測試 | 需要測試 | ||
x86_64 | 完全支持 | 完全支持 | 完全支持 |
FiberTaskingLib 是標準 CMake 建置。但是,有關如何在您自己的專案中建置和包含該程式庫的詳細說明,請參閱文件頁面。
該庫根據 Apache 2.0 許可證獲得許可。但是,FiberTaskingLib 分發並使用來自其他擁有自己授權的開源專案的程式碼:
Boost Context 分支:Boost 授權 v1.0
Catch2:Boost 授權 v1.0
非常歡迎您的貢獻。有關更多詳細信息,請參閱貢獻頁面。
這個實作是我創建的,因為我認為 Christian 的演示非常有趣,我想自己探索一下。該程式碼仍在開發中,我很樂意聽到您對我如何改進它的批評。我將繼續致力於這個項目並盡可能地改進它。