纖程(Fiber)和協程(coroutine)是差不多的概念,也叫做用戶級線程或者輕線程之類的。Windows系統提供了一組API用戶創建和使用纖程,本文中的庫就是基於這組API實現的,所以無法跨平台使用,非Windows程序員可以閃人了,當然如果有興趣可以繼續看下去,找個第三方的協程庫封裝一下,也能實現相同的效果。關於纖程更詳細的信息可以查閱MSDN。
纖程的概念中有兩個關鍵點:
下圖演示了幾個纖程相互切換的過程,注意每個纖程都有獨立的棧,並且通過SwitchToFiber函數切換到其他纖程:
作為對比,我們可以看一下函數調用過程中的堆棧變化情況,下面是示意圖,表示了func1 -> func2 -> func3 這種常見的函數嵌套調用關系:
每一次函數調用都會創建一個新的棧幀(stack frame),合起來就構成整個調用棧,函數返回時其棧幀也隨之釋放。對於函數調用,我們可以確定的一點是(在不拋出異常的情況下)被調用函數執行完畢後一定會在調用點返回並繼續執行下一條語句。但纖程之間的調用(切換)卻不同,一個纖程可以在任意位置切換到其他纖程,並且可能永遠都不會再切換回來,也可能從其他任意纖程(不必是剛剛切換到的)切換回來,前面的示意圖描述的只是一種非常簡單的情況,實際的情況可能非常復雜,復雜到導出都是跳來跳去的箭頭理也理不清。在纖程間切換,有點像用加強版的goto,用的時候固然很爽,但後續的維護卻是個麻煩。
所以就像用while/for/switch-case代替goto一樣,我們也需要封裝一組新的API來代替對操作系統API的直接調用。一方面,在封裝過程中我們可以對纖程的行為(實際是程序員的行為)施加一些安全約束,使得更容易寫出安全的代碼或者更不容易寫出不安全的代碼;另一方面,從goto到while/switch等過程控制語句實際上是一種抽象層次的提升,對大部分常見需求後者用起來更方便,更不容易出錯,寫出的代碼也更簡潔易懂,類似的,從系統API到新的封裝API或者封裝類也是抽象層次的提高,可以更方便的應用在各種業務場景;最後,直接使用系統API需要寫很多維護纖程的輔助代碼,這類代碼通常重復而又分散到業務代碼的各個角落,進一步降低了程序的可讀性和提高了維護難度,封裝也是為了解決這個問題。
好了,廢話說完了,我們先上一段代碼嘗嘗鮮:
1 const int RUN_TIMES = 5; 2 3 int number = 0; 4 bool shutdown = false; 5 6 Fiber fib([&number, &shutdown] 7 { 8 while (!shutdown) 9 { 10 number++; 11 Fiber::yield(); // A:控制權移交到主纖程 12 }13 }); 14 15 for (int i = 0; i < RUN_TIMES; i++) 16 { 17 fib.resume(); // B: 切換到子纖程執行 18 } 19 20 printf("number = %d\r\n", number);
這裡先創建了一個纖程實現number變量累加的功能,然後在for循環中執行(姑且用這個詞)最終得到正確的結果。AB兩處代碼分別實現了纖程的切換,實際上是封裝了對SwitchToFiber的調用,注意兩個函數調用細節上的不同:resume表示切換到對象包裝的纖程,是普通成員函數,yield表示控制權移交給調用者纖程,是靜態成員函數,大家可以思考下為什麼有靜態和非靜態成員函數的差別。
下面是用纖程實現生產者-消費者模型的代碼:
1 int product_count = 0; 2 bool is_end_time = false; 3 4 const int RUN_TIMES = 3; 5 6 // 生產者纖程 7 Fiber fib_producer([&is_end_time, &product_count] 8 { 9 srand((unsigned)time(NULL)); 10 11 while (!is_end_time) 12 { 13 int new_product_count = (int)((double)rand() / RAND_MAX * 10) + 1; 14 product_count += new_product_count; 15 16 printf("[producer] create new products: %d\r\n", new_product_count); 17 18 Fiber::yield(); 19 } 20 21 printf("[producer] off duty.\r\n"); 22 }); 23 24 // 消費者纖程的執行函數 25 auto consumer_proc = [&is_end_time, &product_count](const int seq_number) 26 { 27 int total_count = 0; 28 29 while (!is_end_time) 30 { 31 if (product_count > 0) 32 { 33 product_count--; 34 total_count++; 35 printf("[consumer %d] got 1 product, total got %d, remain %d\r\n", seq_number, total_count, product_count); 36 } 37 38 Fiber::yield(); 39 } 40 41 printf("[consumer %d] off duty.\r\n", seq_number); 42 }; 43 44 const int CONSUMER_COUNT = 3; 45 int consumer_seq_number = 0; 46 47 // 創建消費者纖程數組 48 std::vector<Fiber> consumer_array(CONSUMER_COUNT); 49 std::for_each(consumer_array.begin(), consumer_array.end(), [&](Fiber& item){ item = Fiber([&]{ consumer_proc(consumer_seq_number); }); consumer_seq_number++; }); 50 51 consumer_seq_number = 0; 52 53 for (int i = 0; i < RUN_TIMES; i++) 54 { 55 fib_producer.resume(); 56 57 while (product_count > 0) 58 { 59 consumer_array[consumer_seq_number].resume(); 60 consumer_seq_number = (consumer_seq_number + 1) % CONSUMER_COUNT; 61 } 62 } 63 64 is_end_time = true; 65 66 // 等待纖程結束 67 Fiber::await_all(consumer_array); 68 Fiber::await(fib_producer);
程序末尾出現了await和await_all兩個新的方法可以先不用管,不影響主要邏輯。由於所有纖程都是在同一個線程中運行的所以無需加鎖,這也是使用纖程的一個重要好處,也是我們這個封裝庫的主要目的之一。
限於篇幅,這次就只寫這麼多了,更多的內容將放到後面的帖子中,總計還要寫四、五篇的樣子。但代碼實際上已經寫完了,急性子的園友可以直接到這個地址看代碼:
https://code.csdn.net/xrunning/fiber
建了一個QQ群:微觀架構設計165241092,主要討論C++代碼級設計,感興趣的園友加進來一起討論學習。