天天看點

CSAPP 并發程式設計讀書筆記

并發: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 &amp; .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/多線程/

繼續閱讀