并發:Concurrency,隻要時間上重疊就算并發,可以是單處理器交替處理
并行:Parallel,屬于并發的一種特殊情況(真子集),多核/多 CPU 同時處理
現代作業系統提供了 3 種基本的構造并發程式的方法:
程序:每個邏輯控制流都是一個程序,由核心排程和維護。
I/O 多路複用 :在一個程序上下文中顯式地排程他們自己的邏輯流。邏輯流被模型化為狀态機。
線程:運作在單一程序上下文中的邏輯流,由核心進行排程。可以看作上面兩種方式的混合體(核心排程,但共享同一虛拟位址空間)。
父、子程序中的已連接配接描述符都指向同一個檔案表表項,是以父程序關閉 connfd 至關重要。否則,永遠不會釋放已連接配接描述符 4 connfd 的檔案表條目,引起記憶體洩漏。因為 socket 檔案表表項中的引用計數,直到父子程序 connfd 都關閉了,到用戶端的連接配接才會終止。
共享檔案表,但不共享位址空間(是優點,也是缺點)。不友善共享資料,隻能通過顯式 IPC。程序控制和 IPC 開銷大。
通過 <code>select</code> 函數,等待一組描述符 ready。
<code>select</code> 阻塞,直到至少一個 fd ready(即讀取一個位元組不阻塞)
注意:<code>select</code> 有副作用,會修改入參 fdset 的内容!
單一程序上下文,共享資料容易。
事件驅動,不需要上下文切換,高效,有明顯的性能優勢。
編碼複雜
不能充分利用多核處理器
因為明顯的性能優勢,現代高性能伺服器如 Node.js、nginx 和 Tornado 都是基于 I/O 多路複用的事件驅動
線程由核心自動排程,每個線程有自己的線程上下文。
線程上下文:線程 ID(TID)、棧、棧指針、程式計數器、通用目的寄存器和條件碼。
為了避免 race condition,connfd 必須在堆中建立,線上程中釋放,而不能直接把 connfd 的位址傳給 threadFunc!
每個線程有獨立的線程上下文,共享程序上下文其餘部分,包括整個使用者虛拟位址空間:隻讀文本(代碼.text)、讀/寫資料(.bss & .data)、堆、共享庫代碼和資料。
線程棧不對其他線程設防。
全局變量:定義在函數之外。僅一個執行個體@虛拟記憶體讀/寫區
本地自動變量:定義在函數内,且沒有 static。@虛拟記憶體線程棧
本地靜态變量:定義在函數内,并有 static。僅一個執行個體@虛拟記憶體讀/寫區
C++11 thread_local 存在哪裡?
cnt++ 可以細分 3 個子步驟:加載 L、更新 U、儲存 S。這三個動作必須一次性完成,不可中斷。
進度圖不适用于多處理器。
P(s):若 s 非零,則 s 減 1,立即傳回。若 s 為零,挂起線程,直到 s 變為非零,然後将 s 減 1 傳回。
V(s):将 s 加 1。如果有線程阻塞,則喚醒這些線程中的某一個。
P、V 的加一減一的操作都是原子操作,即 L、U、S 的過程沒有中斷。
如果有多個線程再等待喚醒,V(s) 隻能随機喚醒一個線程,不能指定喚醒哪個線程。
讀者、寫者平等地争奪 w,一旦讀者擷取了 w,将一直占有 w,直到最後一個讀者離開,釋放 w。
如果讀者不斷到達,寫者可能無限等待,導緻饑餓。
以下是一個讀者優先的例子。(弱優先級,當最後一個讀者釋放 w,下一個擷取 w 的不一定是等待 w 的讀者,也有可能是等待 w 的寫者!)
很好的例子,結合上述多種方式的優點,建議親自寫一遍。代碼參考 CSAPP,不再贅述。
通用技術:向對等線程傳遞一個小整數,作為唯一的線程 ID。每個對等線程根據線程 ID 來決定它應該計算序列的哪一部分。
通常每個核上運作一個線程,在一個核上運作運作多個線程會有額外的上下文切換開銷。
多線程求和的例子:
線程數
1
2
4
8
16
sum_mutex
68.00
432.00
719.00
552.00
599.00
sum_global
7.26
3.64
1.91
1.85
1.84
sum_local
1.06
0.54
0.28
0.29
0.30
線程安全(thread-safe):多個并發線程反複調用,結果正确
可重入(reentrant):線程安全的真子集,不需要同步操作,比不可重入的線程安全的函數更高效。
四類不相交的線程不安全函數:
不安全類
說明
例子
變為線程安全的方法
不保護共享變量的函數
-
同步操作保護共享變量;缺點:慢
保持跨越多個調用的狀态的函數
僞随機數生成器:目前調用結果依賴前次調用的中間結果
唯一方式是重寫。不再依賴 static 資料,而是依靠調用者在參數中傳遞狀态
3
傳回指向靜态變量的指針的函數
ctime、gethostbyname:将結果儲存在 static 變量中,然後傳回這個變量的指針
a) 重寫:調用者傳遞存放結果的變量位址; b) 如果難以修改,則建立包裝函數,進行加鎖-複制。
調用線程不安全函數的函數
f 調用線程不安全函數 g
如果 g 是第 2 類,隻能重寫 g;如果 g 是 1、3 類,可以加鎖(拷貝)
大多數 Linux 函數都是線程安全的,包括定義在 C 庫中的函數(例如 malloc、free、realloc、printf、scanf)
線程不安全函數
線程不安全類
Linux 線程安全版本
rand
rand_r
strtok(已棄用)
strtok_r
asctime
asctime_r
ctime
ctime_r
gethostbyaddr(已棄用,推薦 getaddrinfo)
gethostbyaddr_r
gethostbyname(已棄用,推薦 getnameinfo)
gethostbyname_r
net_ntoa(已棄用,推薦 inet_ntop)
無
localtime
localtime_r
分析工具:進度圖
一個死鎖的例子:線程 A 持有 mutex1,等待 mutex2;線程 B 持有 mutex2,等待 mutex1
避免死鎖的最簡單方式——互斥鎖加鎖順序規則:給定所有互斥操作的一個全序,如果每個線程都是以一種順序獲得互斥鎖并以相反的順序釋放,那麼這個程式就是無死鎖的。
注:現代 C++ 可以一次獲得多個鎖,從根源上避免了死鎖。
并發:時間上重疊的邏輯流
三種并發機制:程序、I/O 多路複用和線程
程序由核心排程,獨立虛拟位址空間,隻能顯式 IPC 共享資料
事件驅動程式有自己的并發邏輯流(模型化為狀态機),用 I/O 多路複用來顯式排程這些流
線程:核心自動排程,單一程序上下文
信号量解決共享資料的并發通路問題,提供互斥通路,也支援生産者-消費者、讀者-寫者
被線程調用的函數必須線程安全,有四類線程不安全的函數
可重入函數比不可重入函數更高效,因為不需要任何同步操作
小心競争和死鎖
CSAPP 并發程式設計讀書筆記
現代 C++ 對多線程/并發的支援 A Tour of C++(上)
現代 C++ 對多線程/并發的支援 A Tour of C++(下)
https://www.cnblogs.com/tengzijian/tag/多線程/