第4章 C++多線程系統編程精要

第4章 C++多線程系統編程精要

4.1 引言

學習多線程編程面臨的最大的思維方式的轉變有以下兩點:

  • 當前線程可能隨時會被切換出去,或者說被搶占(preempt)了
  • 多線程程序中事件的發生順序不再有全局統一的先后關系

多線程程序的正確性不能依賴于任何一個線程的執行速度,不能通過原地等待(sleep())來假定其他線程的事件已經發生,而必須通過適當的同步來讓當前線程能看到其他線程的事件的結果。無論線程執行得快與慢(被操作系統切換出去得越多,執行越慢),程序都應該能正常工作。

例如下面這段代碼就有這方面的問題。

bool running = false;//全局標志
void threadFunc() {while(running){//get task from queue}
}
void start() {muduo::Thread t(threadFunc);t.start();running = true;//應該放到t.start()之前
}
  • 這段代碼暗中假定線程函數的啟動慢于running變量的賦值,因此線程函數能進入while循環執行我們想要的功能。
  • 但是,直到有一天,系統負載很高,Thread::start()調用pthread_create()陷入內核后返回時,內核決定換另外一個就緒任務來執行。于是running的賦值就推遲了,這時線程函數就可能不進入while循環而直接退出了。
  • 有人會認為在while之前加一小段延時(sleep)就能解決問題,但這是錯的,無論加多大的延時,系統都有可能先執行while的條件判斷,然后再執行running的賦值。
  • 正確的做法是把running的賦值放到t.start()之前,這樣借助pthread_create()的happens-before語意來保證running的新值能被線程看到。

4.2 基本線程原語的選用

POSIX threads的函數有110多個,真正常用的不過十幾個。而且在C++程序中通常會有更為易用的 wrapper,不會直接調用Pthreads函數。

這11個最基本的Pthreads函數是:

  • 2個:線程的創建和等待結束(join)。封裝為 muduo::Thread。
  • 4個:mutex的創建、銷毀、加鎖、解鎖。封裝為 muduo::MutexLock。
  • 5個:條件變量的創建、銷毀、等待、通知、廣播。封裝為 muduo::Condition。

用這三樣東西(thread、mutex、condition)可以完成任何多線程編程任務。當然我們一般也不會直接使用它們(mutex除外),而是使用更高層的封裝,例如 mutex::ThreadPool 和 mutex::CountDownLatch 等。

除此之外,Pthreads還提供了其他一些原語,有些是可以酌情使用的,有些則是不推薦使用的。

可以酌情使用的有:

  • pthread_once,封裝為muduo::Singleton。其實不如直接用全局變量。
  • pthread_key*,封裝為muduo::ThreadLocal。可以考慮用__thread替換之。

不建議使用:

  • pthread_rwlock,讀寫鎖通常應慎用。muduo沒有封裝讀寫鎖,這是有意的。
  • sem_*,避免用信號量(semaphore)。它的功能與條件變量重合,但容易用錯。
  • pthread_{cancel, kill}。程序中出現了它們,則通常意味著設計出了問題。

不推薦使用讀寫鎖的原因是它往往造成提高性能的錯覺(允許多個線程并發讀),實際上在很多情況下,與使用最簡單的mutex相比,它實際上降低了性能。另外,寫操作會阻塞讀操作,如果要求優化讀操作的延遲,用讀寫鎖是不合適的。

多線程系統編程的難點不在于學習線程原語(primitives),而在于理解多線程與現有的C/C++庫函數和系統調用的交互關系,以進一步學習如何設計并實現線程安全且高效的程序。

4.3 C/C++系統庫的線程安全性

現行的C/C++標準(C89/C99/C++03)并沒有涉及線程。

新版的C/C++標準(C11和C++11)規定了程序在多線程下的語意,C++11還定義了一個線程庫(std::thread)。

對于標準而言,關鍵的不是定義線程庫,而是規定內存模型(memory model)。特別是規定一個線程對某個共享變量的修改何時能被其他線程看到,這稱為內存序(memory ordering)或者內存能見度(memory visibility)。

線程的出現給出現在20世紀90年代Unix操作系統的系統函數庫帶來了沖擊,破壞了20年來一貫的編程傳統和假定。

例如:

  • errno不再是一個全局變量,因為每個線程可能會執行不同的系統庫函數。
  • 有些“純函數”不受影響,例如memset/strcpy/snprintf等等。
  • 有些影響全局狀態或者有副作用的函數可以通過加鎖來實現線程安全,例如malloc/free、printf、fread/fseek等等。
  • 有些返回或使用靜態空間的函數不可能做到線程安全,因此要提供另外的版本,例如asctime_r/ctime_r/gmtime_r、stderror_r、strtok_r等等。
  • 傳統的fork()并發模型不再適用于多線程程序

現在Linux glibc把errno定義為一個宏,注意errno是一個lvalue,因此不能簡單定義為某個函數的返回值,而必須定義為對函數返回指針的dereference。

extern int *__errno_location(void);
#define errno (*__errno_location())

現在glibc庫函數大部分都是線程安全的。特別是FILE*系列函數是安全的,glibc甚至提供了非線程安全的版本以應對某些特殊場合的性能需求。

盡管單個函數是線程安全的,但兩個或多個函數放到一起就不再安全了。

例如fseek()和fread()都是安全的

  • 但是對某個文件“先seek再read”這兩步操作中間有可能會被打斷,其他線程有可能趁機修改了文件的當前位置,讓程序邏輯無法正確執行。
  • 在這種情況下,我們可以用flockfile(FILE*)funlockfile(FILE*)函數來顯式地加鎖。并且由于FILE*的鎖是可重入的,加鎖之后再調用fread()不會造成死鎖。

如果程序直接使用lseek和read這兩個系統調用來隨機讀取文件,也存在“先seek再read”這種race condition,但是似乎我們無法高效地對系統調用加鎖。解決辦法是改用pread系統調用,它不會改變文件的當前位置。

由此可見,編寫線程安全程序的一個難點在于線程安全是不可組合的(composable),一個函數foo()調用了兩個線程安全的函數,而這個foo()函數本身很可能不是線程安全的。即便現在大多數glibc庫函數是線程安全的,我們也不能像寫單線程程序那樣編寫代碼。

例如,在單線程程序中,如果我們要臨時轉換時區,可以用tzset()函數,這個函數會改變程序全局的“當前時區”。

// 保存當前的時區設置
string oldTz = getenv("TZ");
// 設置時區為歐洲倫敦 (Europe/London)
putenv("TZ=Europe/London");
// 更新時區設置
tzset();// 定義一個結構體用于存儲倫敦的本地時間
struct tm localTimeInLN;
// 獲取當前時間戳
time_t now = time(NULL);
// 將當前時間戳轉換為倫敦時區的本地時間,并存儲在localTimeInLN 中
localtime_r(&now, &localTimeInLN);
// 恢復之前保存的時區設置
setenv("TZ", oldTz.c_str(), 1);
// 更新時區設置,使其回到之前的設置
tzset();

但是在多線程程序中,這么做不是線程安全的,即便tzset()本身是線程安全的。

因為它改變了全局狀態(當前時區),這有可能影響其他線程轉換當前時間,或者被其他進行類似操作的線程影響。

解決辦法是使用muduo::TimeZone class,每個immutable instance對應一個時區,這樣時間轉換就不需要修改全局狀態了。

例如:

// 自定義 TimeZone 類
class TimeZone {
public:// 構造函數,接受時區文件路徑explicit TimeZone(const char* zonefile);// 將時間戳轉換為特定時區的本地時間struct tm toLocalTime(time_t secondsSinceEpoch) const;// 將特定時區的本地時間轉換為時間戳time_t fromLocalTime(const struct tm&) const;// 其他可能的成員函數...
};// 定義常量表示紐約時區和倫敦時區
const TimeZone kNewYorkTz("/usr/share/zoneinfo/America/New_York");
const TimeZone kLondonTz("/usr/share/zoneinfo/Europe/London");// 獲取當前時間戳
time_t now = time(NULL);
// 將當前時間戳轉換為紐約時區的本地時間
struct tm localTimeInNY = kNewYorkTz.toLocalTime(now);
// 將當前時間戳轉換為倫敦時區的本地時間
struct tm localTimeInLN = kLondonTz.toLocalTime(now);

一個基本思路是盡量把class設計成immutable的,這樣用起來就不必為線程安全操心了。

盡管C++03標準沒有明說標準庫的線程安全性,但我們可以遵循

  • 一個基本原則:凡是非共享的對象都是彼此獨立的,如果一個對象從始至終只被一個線程用到,那么它就是安全的。
  • 一個事實標準:共享的對象的read-only操作是安全的,前提是不能有并發的寫操作。

例如:

  • 兩個線程各自訪問自己的局部vector對象是安全的;
  • 同時訪問共享的const vector對象也是安全的,但是這個vector不能被第三個線程修改。一旦有writer,那么read-only操作也必須加鎖,例如vector::size()。

C++的標準庫容器和std::string都不是線程安全的,只有std::allocator保證是線程安全的。一方面的原因是為了避免不必要的性能開銷,另一方面的原因是單個成員函數的線程安全并不具備可組合性(composable)。

假設有safe_vectorclass,它的接口與std::vector相同,不過每個成員函數都是線程安全的(類似Javasynchronized方法)。但是用safe_vector并不一定能寫出線程安全的代碼。

例如:

safe_vector<int> vec;//全局可見
if(!vec.empty()) { //沒有加鎖保護int x = vec[0];//這兩步在多線程下是不安全的
}

在if語句判斷vec非空之后,別的線程可能清空其元素,從而造成vec[0]失效。

C++標準庫中的絕大多數泛型算法是線程安全的,因為這些都是無狀態純函數。只要輸入區間是線程安全的,那么泛型函數就是線程安全的。

C++的iostream不是線程安全的,因為流式輸出

std::cout << "Now is " << time(NULL);

等價于兩個函數調用

std::cout.operator<<("Now is ").operator<<(time(NULL));

即便ostream::operator<<()做到了線程安全,也不能保證其他線程不會在兩次函數調用之前向stdout輸出其他字符。

對于“線程安全的stdout輸出”這個需求,我們可以改用printf,以達到安全性和輸出的原子性。但是這等于用了全局鎖,任何時刻只能有一個線程調用printf,恐怕不見得高效。

4.4 Linux上的線程標識

POSIX threads庫提供了pthread_self函數用于返回當前進程的標識符,其類型為pthread_t。pthread_t不一定是一個數值類型(整數或指針),也有可能是一個結構體,因此Pthreads專門提供了pthread_equal函數用于對比兩個線程標識符是否相等。

這就帶來一系列問題,包括:

  • 無法打印輸出pthread_t,因為不知道其確切類型。也就沒法在日志中用它表示當前線程的id。
  • 無法比較pthread_t的大小或計算其hash值,因此無法用作關聯容器的key。
  • 無法定義一個非法的pthread_t值,用來表示絕對不可能存在的線程id,因此MutexLock class沒有辦法有效判斷當前線程是否已經持有本鎖。
  • pthread_t值只在進程內有意義,與操作系統的任務調度之間無法建立有效關聯。比方說在/proc文件系統中找不到pthread_t對應的task。

glibc的Pthreads實現實際上把pthread_t用作一個結構體指針(它的類型是unsigned long),指向一塊動態分配的內存,而且這塊內存是反復使用的。

這就造成pthread_t的值很容易重復。Pthreads只保證同一進程之內,同一時刻的各個線程的id不同;不能保證同一進程先后多個線程具有不同的id,更不要說一臺機器上多個進程之間的id唯一性了。

例如下面這段代碼中先后兩個線程的標識符是相同的:

int main(){pthread_t t1,t2;pthread_create(&t1,NULL,threadFunc,NULL);printf("%lx\n",t1);pthread_join(t1,NULL);pthread_create(&t2,NULL,threadFunc,NULL);printf("%lx\n",t2);pthread_join(t2,NULL);
}
$ ./a.out
7fad11787700
7fad11787700

因此,pthread_t并不適合用作程序中對線程的標識符。

在Linux上,作者建議使用gettid系統調用的返回值作為線程id,這么做的好處有:

  • 它的類型是pid_t,其值通常是一個小整數13,便于在日志中輸出。
  • 在現代Linux中,它直接表示內核的任務調度id,因此在/proc文件系統中可以輕易找到對應項:/proc/tid或/prod/pid/task/tid。
  • 在其他系統工具中也容易定位到具體某一個線程,例如在top中我們可以按線程列出任務,然后找出CPU使用率最高的線程id,再根據程序日志判斷到底哪一個線程在耗用CPU。
  • 任何時刻都是全局唯一的,并且由于Linux分配新pid采用遞增輪回辦法,短時間內啟動的多個線程也會具有不同的線程id。
  • 0是非法值,因為操作系統第一個進程init的pid是1。

但是glibc并沒有封裝這個系統調用,需要我們自己實現。

作者封裝的gettid的方法如下:

muduo::CurrentThread::tid()采取的辦法是用__thread變量來緩存gettid的返回值,這樣只有在本線程第一次調用的時候才進行系統調用,以后都是直接從thread local緩存的線程id拿到結果,效率無憂。

未完待續。。。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/168182.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/168182.shtml
英文地址,請注明出處:http://en.pswp.cn/news/168182.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

軟著項目推薦 深度學習 opencv python 實現中國交通標志識別

文章目錄 0 前言1 yolov5實現中國交通標志檢測2.算法原理2.1 算法簡介2.2網絡架構2.3 關鍵代碼 3 數據集處理3.1 VOC格式介紹3.2 將中國交通標志檢測數據集CCTSDB數據轉換成VOC數據格式3.3 手動標注數據集 4 模型訓練5 實現效果5.1 視頻效果 6 最后 0 前言 &#x1f525; 優質…

游覽器緩存講解

瀏覽器緩存是指瀏覽器在本地存儲已經請求過的資源的一種機制&#xff0c;以便在將來的請求中能夠更快地獲取這些資源&#xff0c;減少對服務器的請求&#xff0c;提高頁面加載速度。瀏覽器緩存主要涉及到兩個方面&#xff1a;緩存控制和緩存位置。 緩存控制 Expires 頭&#…

Javascript每天一道算法題(十六)——獲取除自身以外數組的乘積_中等

文章目錄 1、問題2、示例3、解決方法&#xff08;1&#xff09;方法1 總結 1、問題 給你一個整數數組 nums&#xff0c;返回 數組 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘積 。 題目數據 保證 數組 nums之中任意元素的全部前綴元素和后綴…

RAM模型從數據準備到pretrain、finetune與推理全過程詳細說明

提示&#xff1a;RAM模型&#xff1a;環境安裝、數據準備與說明、模型推理、模型finetune、模型pretrain等 文章目錄 前言一、環境安裝二、數據準備與解讀1.數據下載2.數據標簽內容解讀3.標簽map內容解讀 三、finetune訓練1.微調訓練命令2.load載入參數問題3.權重載入4.數據加載…

使用new Vue()的時候發生了什么?

前言 Vue.js是一個流行的JavaScript前端框架&#xff0c;用于構建單頁面應用&#xff08;SPA&#xff09;和用戶界面。當我們使用new Vue()來創建一個Vue實例時&#xff0c;Vue會執行一系列的初始化過程&#xff0c;將數據變成響應式&#xff0c;編譯模板&#xff0c;掛載實例…

RabbitMQ之發送者(生產者)可靠性

文章目錄 前言一、生產者重試機制二、生產者確認機制實現生產者確認&#xff08;1&#xff09;定義ReturnCallback&#xff08;2&#xff09;定義ConfirmCallback 總結 前言 生產者重試機制、生產者確認機制。 一、生產者重試機制 問題&#xff1a;生產者發送消息時&#xff0…

分布式事務總結

文章目錄 一、分布式事務基礎什么是事務&#xff1f;本地事物分布式事務分布式事務的場景 二、分布式事務解決方案全局事務可靠消息服務TCC 事務 三、Seata 分布式事務解決方案3.1 Seata-At模式3.2 秒殺項目集成 Seata啟動 Seata-Server項目集成seata配置AT模式代碼實現 3.3 Se…

openstack(2)

目錄 塊存儲服務 安裝并配置控制節點 安裝并配置一個存儲節點 驗證操作 封裝鏡像 上傳鏡像 塊存儲服務 安裝并配置控制節點 創建數據庫 [rootcontroller ~]# mysql -u root -pshg12345 MariaDB [(none)]> CREATE DATABASE cinder; MariaDB [(none)]> GRANT ALL PR…

1、Docker概述與安裝

相關資源網站&#xff1a; ● docker官網&#xff1a;http://www.docker.com ● Docker Hub倉庫官網: https://hub.docker.com/ 注意&#xff0c;如果只是想看Docker的安裝&#xff0c;可以直接往下拉跳轉到Docker架構與安裝章節下的Docker具體安裝步驟&#xff0c;一步步帶你安…

82基于matlab GUI的圖像處理

基于matlab GUI的圖像處理&#xff0c;功能包括圖像一般處理&#xff08;灰度圖像、二值圖&#xff09;&#xff1b;圖像幾何變換&#xff08;旋轉可輸入旋轉角度、平移、鏡像&#xff09;、圖像邊緣檢測&#xff08;拉普拉斯算子、sobel算子、wallis算子、roberts算子&#xf…

【Rust日報】2023-11-22 Floneum -- 基于 Rust 的一款用于 AI 工作流程的圖形編輯器

Floneum -- 基于 Rust 的一款用于 AI 工作流程的圖形編輯器 Floneum 是一款用于 AI 工作流程的圖形編輯器&#xff0c;專注于社區制作的插件、本地 AI 和安全性。 Floneum 有哪些特性&#xff1a; 可視化界面&#xff1a;您無需任何編程知識即可使用Floneum。可視化圖形編輯器可…

oled的使用 動態的變量 51

源碼均在IIC手寫程序中 外部中斷實現變量加一 #include "reg52.h" #include "main.h" #include <intrins.h> #include "OLED.h" #include "bmp.h" #include "Delay.h" sbit LED1 P1^0; sbit LED2 P1^1; sbit LED3…

【LeetCode每日一題】525. 連續數組

題目&#xff1a; 給定一個二進制數組 nums , 找到含有相同數量的 0 和 1 的最長連續子數組&#xff0c;并返回該子數組的長度。 媽的 連題目都沒有讀懂&#xff01;本來看成是找到兩個連續子數組&#xff0c;兩個連續子數組的 0 1 個數分別相同&#xff0c;我說怎么看著如此…

Python報錯:AttributeError(類屬性、實例屬性)

Python報錯&#xff1a;AttributeError&#xff08;類屬性、實例屬性&#xff09; Python報錯&#xff1a;AttributeError 這個錯誤就是說python找不到對應的對象的屬性&#xff0c;百度后才發現竟然是初始化類的時候函數名寫錯了 __init__應該有2條下劃線&#xff0c;如果只有…

構建未來:云計算 生成式 AI 誕生科技新局面

目錄 引言生成式 AI&#xff1a;開發者新伙伴云計算與生成式 AI 的無縫融合亞馬遜云與生成式 AI 結合的展望/總結我用亞馬遜云科技生成式 AI 產品打造了什么&#xff0c;解決了什么問題未來科技發展趨勢&#xff1a;開發者的機遇與挑戰結合實踐看未來結語開源項目 引言 2023年…

SpectralGPT: Spectral Foundation Model 論文翻譯1

遙感領域的通用大模型 2023.11.13在CVPR發表 原文地址&#xff1a;[2311.07113] SpectralGPT: Spectral Foundation Model (arxiv.org) 摘要 ? 基礎模型最近引起了人們的極大關注&#xff0c;因為它有可能以一種自我監督的方式徹底改變視覺表征學習領域。雖然大多數基礎模型…

VSCode 連接遠程服務器問題及解決辦法

端口號不一樣&#xff0c;需要在配置文件中添加Port Host 27.223.26.46HostName 27.223.*.*User userForwardAgent yesPort 14111輸入密碼后可以連接 在vscode界面&#xff0c;終端&#xff0c;生成公鑰&私鑰 ssh-keygen可以看到有id_rsa和id_rsa.pub兩個文件生成&#…

curl 命令的一些基本用法,

curl 是一個用于在命令行中進行網絡請求的工具。以下是一些 curl 命令的常見用法&#xff1a; 從 URL 下載文件并保存為本地文件&#xff1a; curl -O URL例如&#xff1a; curl -O https://example.com/file.zip這將會將 file.zip 下載到當前目錄。 將文件下載到指定位置&…

Nginx如何配置負載均衡

nginx的負載均衡有4種模式&#xff1a; 1)、輪詢&#xff08;默認&#xff09; 每個請求按時間順序逐一分配到不同的后端服務器&#xff0c;如果后端服務器down掉&#xff0c;能自動剔除。 2)、weight 指定輪詢幾率&#xff0c;weight和訪問比率成正比&#xff0c;用于后端服務…

C#,《小白學程序》第五課:隊列(Queue)其一,排隊的技術與算法

日常生活中常見的排隊&#xff0c;軟件怎么體現呢&#xff1f; 排隊的基本原則是&#xff1a;先到先得&#xff0c;先到先吃&#xff0c;先進先出 1 文本格式 /// <summary> /// 《小白學程序》第五課&#xff1a;隊列&#xff08;Queue&#xff09; /// 日常生活中常見…