簡單的線程池(三)

概要

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

目的

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

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

環境

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

  • 硬件配置: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重印) 一書中的部分概念及思路。致段念。