簡單的執行緒池(三)

cnblogs發表於2021-12-08

概要

本文中,作者針對 簡單的執行緒池簡單的執行緒池(二) 介紹的兩個執行緒池分別進行了併發測試,並基於收集的測試資料,對結果進行了分析。

目的

本測試是為了確認非阻塞式執行緒池阻塞式執行緒池的生存性,以及兩者在吞吐量上的差異,為改進執行緒池提供資料支撐。

【注】這裡的差異以非阻塞式的吞吐量為基準計算得出的,即 (阻塞式吞吐量 - 非阻塞式吞吐量) ÷ 非阻塞式吞吐量 的百分比。類似併發、壓力之類的測試依賴於測試環境,因此筆者認為兩者在量級上的差異比絕對資料更有意義。

環境

考慮到兩個執行緒池的簡單程度,為易於顯示兩者之間的差異,筆者選擇了硬體配置偏低的測試環境,

  • 硬體配置:Raspberry Pi 3 Model B
    • Quad Core 1.2GHz 64bit
    • 1G RAM
    • 16G MicroSD
    • 100 Base Ethernet
  • 軟體配置:Raspbian Stretch
    • g++ (Raspbian 6.3.0-18+rpi1+deb9u1) 6.3.0 20170516

用例

針對兩個執行緒池,測試過程模擬出 10 個使用者(執行緒)向執行緒池提交任務,分別實施如下 15 個測試用例,

編號 提交週期(分鐘) 思考時間(毫秒)
1 0.5 0
2 0.5 0 ~ 8 隨機
3 0.5 0 ~ 32 隨機
4 0.5 0 ~ 128 隨機
5 0.5 0 ~ 1024 隨機
6 1 0
7 1 0 ~ 8 隨機
8 1 0 ~ 32 隨機
9 1 0 ~ 128 隨機
10 1 0 ~ 1024 隨機
11 3 0
12 3 0 ~ 8 隨機
13 3 0 ~ 32 隨機
14 3 0 ~ 128 隨機
15 3 0 ~ 1024 隨機

收集如下測試資料,

  • 提交的任務總數 (A): 在提交週期內,所有使用者提交的任務總數;
  • 剩餘的任務總數 (B): 使用者結束提交時,執行緒池中剩餘的任務總數;
  • 總時長 (C): 從提交任務開始到處理完所有任務之間的總時間,精確到千分之一秒。

得出 3 個吞吐量指標(任務數 ÷ 秒),

  • 從開始提交任務到結束提交任務期間的吞吐量(1): (A - B)÷(併發週期 × 60);
  • 從結束提交任務開始到處理完所有任務期間的吞吐量(2): B ÷(總時長 - 併發週期 × 60);
  • 從開始提交任務到到處理完所有任務期間的吞吐量(3): A ÷ 總時長。

如果把執行緒池接受任務的過程稱為“吞”,執行緒池分派任務的過程稱為“吐”,則根據前述吞吐量 1 ~ 3 的定義可以看出,吞吐量1 代表的是執行緒池在有使用者提交任務的時間段內的 “吞” + “吐” 的絕對能力;吞吐量2 代表的是執行緒池在無使用者提交任務的時間段內的 “吐” 的絕對能力;吞吐量3 代表的是執行緒池在 有提交任務 + 沒提交任務 的時間段內的 ”吞“ + ”吐“ 的整體能力。

每個測試用例執行 10 次,執行結果的平均值作為某指標的平均吞吐量。為了降低 I/O 對併發能力的影響,程式中任務輸出到 stdout 的內容都被重定向到 /dev/null,僅將 stderr 的內容輸出到終端。

測試用例執行檔案:

  • 非阻塞式: lockwise_test.cpp
  • 阻塞式: blocking_test.cpp

測試

  1. 根據測試用例的要求,修改測試用例檔案中的引數,

    • 提交週期: 修改 PERIOD 的初始值為 0.5,1 或 3;
    • 思考時間: 在使用者執行緒的初始函式中,放開或註釋以下內容,
      • std::this_thread::yield(),則思考時間為 0;
      • std::this_thread::sleep_for(milliseconds(rand()%RAND_LIMIT)),則思考時間為 0 ~ RAND_LIMIT 毫秒隨機(須同時修改 RAND_LIMIT 的初始值為 8,32,128 或 1024)。
  2. 編譯測試用例執行檔案

    • 非阻塞式: g++ -std=c++11 -lpthread lockwise_test.cpp
    • 阻塞式: g++ -std=c++11 -lpthread blocking_test.cpp
  3. 執行

    • ./a.out 1>/dev/null

結果

  • 用例 1 的結果

  • 用例 2 的結果

  • 用例 3 的結果

  • 用例 4 的結果

  • 用例 5 的結果

  • 用例 6 的結果

  • 用例 7 的結果

  • 用例 8 的結果

  • 用例 9 的結果

  • 用例 10 的結果

  • 用例 11 的結果


    程式要求的記憶體容量超過了作業系統可分配的實體記憶體,丟擲了 std::bad_alloc 異常。

  • 用例 12 的結果

  • 用例 13 的結果

  • 用例 14 的結果

  • 用例 15 的結果

分析

圖1 ~ 圖3 彙總了測試用例 1 ~ 15 的結果中平均吞吐量資料和差異,


圖1


圖2


圖3


圖4
在 圖4 中列舉了 吞吐量1 的差異在 0.5 分鐘、1 分鐘和 3 分鐘內不同思考時間上的對比。可以看到,
  • 當思考時間為 0 時,阻塞式的吞吐量略微優於非阻塞式的吞吐量;延長提交週期後,阻塞式的吞吐量明顯優於非阻塞式的吞吐量;
  • 當思考時間不為 0 時,阻塞式的吞吐量大幅優於非阻塞式的吞吐量,但差異不會因提交週期的延長而大幅變化;隨著思考時間的增加,阻塞式的吞吐量與非阻塞式的吞吐量之間的差異逐漸消失。


圖5
在 圖5 中列舉了 吞吐量2 的差異在 0.5 分鐘、1 分鐘和 3 分鐘內不同思考時間上的對比。可以看到,
  • 當思考時間為 0 時,阻塞式的吞吐量劣於非阻塞式的吞吐量;延長提交週期後,阻塞式的吞吐量明顯劣於非阻塞式的吞吐量;
  • 當思考時間不為 0 時,因阻塞式的吞吐量和非阻塞式的吞吐量均為 0,它們間沒有差異。


圖6
在 圖6 中列舉了 吞吐量3 的差異在 0.5 分鐘、1 分鐘和 3 分鐘內不同思考時間上的對比。可以看到,
  • 當思考時間為 0 時,阻塞式的吞吐量略微優於非阻塞式的吞吐量;延長提交週期後,阻塞式的吞吐量優於非阻塞式的吞吐量;
  • 當思考時間不為 0 時,阻塞式的吞吐量大幅優於非阻塞式的吞吐量,但差異不會因提交週期的延長而大幅變化;隨著思考時間的增加,阻塞式的吞吐量與非阻塞式的吞吐量之間的差異逐漸消失。

考慮到現實中的思考時間為 0 的情況相當少見,基於上述的分析,筆者認為,

  • 在需要應對高頻併發的場合,採用阻塞式執行緒池的效能會優於非阻塞式執行緒池的效能;
  • 在需要應對低頻併發的場合,採用阻塞式執行緒池的效能相當於非阻塞式執行緒池的效能;
  • 在僅為分派併發任務的場合,採用阻塞式執行緒池的效能會劣於非阻塞式執行緒池的效能。

最後

完整測試程式碼及測試資料請參考 [github] cnblogs/15622669

筆者參考了 軟體效能測試過程詳解與案例剖析 / 段念 編著. - 2版. - 北京: 清華大學出版社, 2012.6 (2020.4重印) 一書中的部分概念及思路。致段念。

相關文章