溫故Linux后端編程(三):線程那些事兒
文章目錄
前言
摘要
線程
什么是線程
使用線程的優勢
線程與進程千絲萬縷的糾纏
線程間資源共享情況
使用線程的弊端
線程管理(Thread Managment)
創建線程
獲取當前線程id
判斷倆線程是否相等
連接(Joining)和分離(Detaching)線程
線程屬性
互斥量
互斥量存在的意義
互斥鎖原語
參數釋義
互斥量使用
死鎖
鎖種
樂觀鎖
悲觀鎖
樂觀鎖 VS 悲觀鎖
自旋鎖 && 互斥鎖
條件變量
條件變量原語
條件變量與互斥鎖
注意事項
虛假喚醒與喚醒丟失
⑴虛假喚醒
⑵喚醒丟失
使用條件變量
線程池
番外篇
Pthread API函數
多線程下的對象創建
對象的銷毀與競態條件
shared_ptr/weak_ptr
再聊會兒C++內存安全
資源推薦
前言
不知不覺,就到大三了。
不知不覺,就要開始找暑期實習了。
溫故而知新嘛。(數據結構復習兩天發現不對,我還是更喜歡這個。) 所以就來了。
摘要
在多處理器共享內存的架構中(如:對稱多處理系統SMP),線程可以用于實現程序的并行性。歷史上硬件銷售商實現了各種私有版本的多線程庫,使得軟件開發者不得不關心它的移植性。對于UNIX系統,IEEE POSIX 1003.1標準定義了一個C語言多線程編程接口。依附于該標準的實現被稱為POSIX theads 或 Pthreads。
該教程介紹了Pthreads的概念、動機和設計思想。內容包含了Pthreads API主要的三大類函數:線程管理(Thread Managment)、互斥量(Mutex Variables)和條件變量(Condition Variables)。向剛開始學習Pthreads的程序員提供了演示例程。
適于:剛開始學習使用線程實現并行程序設計;對于C并行程序設計有基本了解。
線程
都說知其然,知其所以然。
不知道,我們專業的要求是這樣的。
什么是線程
官方話就是:是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發多個線程,每條線程并行執行不同的任務。
1、提高程序的并發性 2、開銷小,不需要重新分配內存 3、通信和共享數據方便
1
2
3
使用線程的優勢
在同一個進程中的所有線程共享同樣的地址空間。較于進程間的通信,在許多情況下線程間的通信效率比較高,且易于使用。
較于沒有使用線程的程序,使用線程的應用程序有潛在的性能增益和實際的優點:
CPU使用I/O交疊工作:例如,一個程序可能有一個需要較長時間的I/O操作,當一個線程等待I/O系統調用完成時,CPU可以被其它線程使用。
優先/實時調度:比較重要的任務可以被調度,替換或者中斷較低優先級的任務。
異步事件處理:頻率和持續時間不確定的任務可以交錯。例如,web服務器可以同時為前一個請求傳輸數據和管理新請求。
Pthreads沒有中間的內存復制,因為線程和一個進程共享同樣的地址空間。沒有數據傳輸。變成cache-to-CPU或memory-to-CPU的帶寬(最壞情況),速度是相當的快。
劣勢啊,劣勢也很明顯,毀譽參半,后面再說。
線程與進程千絲萬縷的糾纏
(1)線程又被叫做輕量級進程,也有PCB,創建線程使用的底層函數和進程是一樣的,都是clone。 (2)從內核里看線程和進程是一樣的,都有各自不同的PCB,但是PCB指向的內存資源的三級頁表是不同的。 (3)進程可以蛻變成線程,進程也可以說是主線程,就是高速路的主干道。 (4)在linux下,線程是最小的執行單位,進程是最小的分配資源單位。
1
2
3
4
線程間資源共享情況
⑴共享資源
1、文件描述符表 2、每種信號的處理方式 3、當前工作目錄 4、用戶ID和組ID 5、內存地址空間
1
2
3
4
5
⑵非共享資源
1、線程id 2、處理器現場和棧指針 3、獨立的棧空間 4、errno變量 5、信號屏蔽字 6、調度優先級
1
2
3
4
5
6
使用線程的弊端
1、線程不穩定(這個是真的不穩定,后面會專門出一篇“可重入函數對線程的影響”,因為現在還沒整理好那塊兒) 2、線程調試困難(這個是真的頭疼,難以調試的東西,目前我只有一個“段錯誤,核心已轉儲”可以用用,關鍵是錯誤難以復現,很難,很難) 3、線程無法使用Unix經典事件,如信號(這個反正我也沒用過,管它)
1
2
3
例如:假設你的程序創建了幾個線程,每一個調用相同的庫函數:
這個庫函數存取/修改了一個全局結構或內存中的位置。
當每個線程調用這個函數時,可能同時去修改這個全局結構活內存位置。
如果函數沒有使用同步機制去阻止數據破壞,這時,就不是線程安全的了。
如果你不是100%確定外部庫函數是線程安全的,自己負責所可能引發的問題。 建議:小心使用庫或者對象,當不能明確確定是否是線程安全的。若有疑慮,假設其不是線程安全的直到得以證明。 可以通過不斷地使用不確定的函數找出問題所在。
1
2
3
看一下這篇(過幾天會重寫):可重入函數對于線程安全的意義
線程管理(Thread Managment)
創建線程
#include
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Q:怎樣安全地向一個新創建的線程傳遞數據?
A:確保所傳遞的數據是線程安全的(不能被其他線程修改)。下面三個例子演示了那個應該和那個不應該。
代碼演示:
// Example Code - Pthread Creation and Termination #include 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 接下來演示線程安全: //下面的代碼片段演示了如何向一個線程傳遞一個簡單的整數。 //主線程為每一個線程使用一個唯一的數據結構,確保每個線程傳遞的參數是完整的。 int *taskids[NUM_THREADS]; for(t=0; t 1 2 3 4 5 6 7 8 9 10 11 12 //例子展示了用結構體向線程設置/傳遞參數。每個線程獲得一個唯一的結構體實例。 struct thread_data{ int thread_id; int sum; char *message; }; struct thread_data thread_data_array[NUM_THREADS]; void *PrintHello(void *threadarg) { struct thread_data *my_data; ... my_data = (struct thread_data *)threadarg; taskid = my_data->thread_id; sum = my_data->sum; hello_msg = my_data->message; ... } int main (int argc, char *argv[]) { ... thread_data_array[t].thread_id = t; thread_data_array[t].sum = sum; thread_data_array[t].message = messages[t]; rc = pthread_create(&threads[t], NULL, PrintHello,(void *) &thread_data_array[t]); ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 //例子演示了錯誤地傳遞參數。循環會在線程訪問傳遞的參數前改變傳遞給線程的地址的內容。 int rc, t; for(t=0; t 1 2 3 4 5 6 7 8 9 獲取當前線程id #include 1 2 3 線程id的類型是pthread_t,它在當前進程中是唯一的,但是在不同系統中這個類型有不同的實現,它可能是一個整數值,也可能是一個結構體,反正就是你猜不到的東西。 判斷倆線程是否相等 #include 1 2 3 注意這兩個函數中的線程ID對象是不透明的,不是輕易能檢查的。因為線程ID是不透明的對象,所以C語言的==操作符不能用于比較兩個線程ID。 連接(Joining)和分離(Detaching)線程 pthread_join(threadid,status) pthread_detach(threadid,status) pthread_attr_setdetachstate(attr,detachstate) pthread_attr_getdetachstate(attr,detachstate) 1 2 3 4 5 6 7 pthread_join()函數阻塞調用線程直到threadid所指定的線程終止。 如果在目標線程中調用pthread_exit(),程序員可以在主線程中獲得目標線程的終止狀態。 連接線程只能用pthread_join()連接一次。若多次調用就會發生邏輯錯誤。 兩種同步方法,互斥量(mutexes)和條件變量(condition variables),稍后討論。 1 2 3 4 5 6 可連接(Joinable or Not)? 當一個線程被創建,它有一個屬性定義了它是可連接的(joinable)還是分離的(detached)。 只有是可連接的線程才能被連接(joined),若果創建的線程是分離的,則不能連接。 POSIX標準的最終草案指定了線程必須創建成可連接的。然而,并非所有實現都遵循此約定。 使用pthread_create()的attr參數可以顯式的創建可連接或分離的線程 1 2 3 4 典型四步如下: 聲明一個pthread_attr_t數據類型的線程屬性變量 用 pthread_attr_init()初始化改屬性變量 用pthread_attr_setdetachstate()設置可分離狀態屬性 完了后,用pthread_attr_destroy()釋放屬性所占用的庫資源 1 2 3 4 5 6 7 分離(Detaching): pthread_detach()可以顯式用于分離線程,盡管創建時是可連接的。 沒有與pthread_detach()功能相反的函數 又到了演示線程安全的時間了 //這個例子演示了用Pthread join函數去等待線程終止。 //因為有些實現并不是默認創建線程是可連接狀態,例子中顯式地將其創建為可連接的。 #include 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 當一個線程被設置為分離線程時,如果線程的運行非常快,可能在pthread_create()函數返回之前就終止了。由于一個線程在終止以后可以將線程號和系統資源移交給其他的線程使用,此時再使用函數pthread_cretae()獲得的線程號進行操作將會發生錯誤。 線程屬性 linux下線程屬性是可以根據實際項目需要進行設置。 之前我們討論的都是線程的默認屬性,默認屬性已經可以解決大部分線程開發時的需求。 如果需要更高的性能,就需要人為對線程屬性進行配置。 typedef struct { int detachstate; //線程的分離狀態 int schedpolicy; //線程的調度策略 struct sched schedparam;//線程的調度參數 int inheritsched; //線程的繼承性 int scope; //線程的作用域 size_t guardsize; //線程棧末尾的警戒緩沖區大小 int stackaddr_set; //線程棧的設置 void* stackaddr; //線程棧的啟始位置 size_t stacksize; //線程棧大小 }pthread_attr_t; //在上面我們可以看到,關于這個結構體中的相關參數 1 2 3 4 5 6 7 8 9 10 11 12 13 14 默認的屬性為非綁定、非分離、缺省的堆棧、與父進程同樣級別的優先級。 線程屬性設置的一般套路: 第一:定義屬性變量并初始化 pthread_attr_t pthread_attr_init() 第二:調用你想設置的屬性的接口函數 pthread_attr_setxxxxxxxx() 第三:創建線程的時候,第二個參數使用這個屬性 第四:銷毀屬性 pthread_destroy(); 1 2 3 4 5 6 7 8 互斥量 互斥量存在的意義 做個小實驗吧,兩個線程計數。如果最后加起來是20萬那就不用往下看了。 #include 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 好,為什么要線程同步,那就心照不宣了 算了,官方話還是要說一說的 1、共享資源,多個線程都可以對共享資源進行操作 2、線程操作共享資源的先后順序不一定 3、處理器對存儲器的操作一般不是原子操作 互斥鎖原語 pthread_mutex_t mutex = PTHREAD_MUREX_INITALIZER //用于初始化互斥鎖,后面簡稱鎖 int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); //初始化鎖,和上面那個一個意思。 //初始化一個互斥鎖(互斥量)–>初值可看做1 int pthread_mutex_destroy(pthread_mutex_t *mutex); //銷毀鎖 int pthread_mutex_lock(pthread_mutex_t *mutex); //上鎖 int pthread_mutex_unlok(pthread_mutex_t *mutex); //解鎖 int pthread_mutex_trylock(pthread_mutex_t *mutex); //嘗試上鎖 1 2 3 4 5 6 7 8 參數釋義 <這里只釋義那個init> 參數1:傳出參數,調用時應傳&mutex restrict關鍵字:只用于限制指針,告訴編譯器,所有修改該指針指向內存中內容的操作,只能通過本指針完成。不能通過除本指針以外的其他變量或指針修改。 參數2:互斥屬性。是一個傳入參數,通常傳NULL,選用默認屬性(線程間共享). 靜態初始化:如果互斥鎖mutex是靜態分配的(定義在全局,或加了static關鍵字修飾),可以直接使用宏進行初始化。pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 動態初始化:局部變量應采用動態初始化。pthread_mutex_init(&mutex, NULL); attr對象用于設置互斥量對象的屬性,使用時必須聲明為pthread_mutextattr_t類型,默認值可以是NULL。Pthreads標準定義了三種可選的互斥量屬性: 協議(Protocol): 指定了協議用于阻止互斥量的優先級改變 優先級上限(Prioceiling):指定互斥量的優先級上限 進程共享(Process-shared):指定進程共享互斥量 注意所有實現都提供了這三個可選的互斥量屬性。 Q:有多個線程等待同一個鎖定的互斥量,當互斥量被解鎖后,那個線程會第一個鎖定互斥量? A:除非線程使用了優先級調度機制,否則,線程會被系統調度器去分配,那個線程會第一個鎖定互斥量是隨機的。 1 2 3 互斥量使用 #include 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 拿去執行,如果不是20萬也可以不用往下看了。 死鎖 (上一篇 進程·全家桶 在這個問題上花了不少篇幅) 為什么我要強調上鎖和解鎖一定要放在一起寫,就是防止出現人為失誤導致死鎖 死鎖嘛,解不開了。 要么是你忘了解開,別人也就沒得用了 要么就是幾個線程互相掐著關鍵數據導致誰也沒辦法完成任務,結果誰也沒辦法解鎖。 這種情況下只有銷毀掉代價最小的那個鎖,讓任務執行下去,不過后面要記得把那個被銷毀的任務重新運作。 鎖種 樂觀鎖,你看它名字就知道,把事情想得很單純,它總認為資源和數據不會被別人所修改,所以讀取不會上鎖,但是樂觀鎖在進行寫入操作的時候會判斷當前數據是否被修改過。可以使用版本號等機制。 樂觀鎖多適用于多度的應用類型,這樣可以提高吞吐量。 使用自增長的整數表示數據版本號: 若這雙寫互不干擾,男的取出,版本號為0,男的寫入,版本號+1;女的取出,版本號為1,女的寫入,版本號為2。 若這雙寫相互干擾了,男的取出,版本號為0;男的還沒寫入,女的取出,版本號為0;男的寫入,版本號為1;女的寫入,發現版本號不匹配,則寫入失敗,應該重新讀取金額數和版本號。 此外,也可以通過時間戳來實現 悲觀鎖是一種悲觀思想,它總認為最壞的情況可能會出現,它認為數據很可能會被其他人所修改,所以悲觀鎖在持有數據的時候總會把資源 或者 數據 鎖住,這樣其他線程想要請求這個資源的時候就會阻塞,直到等到悲觀鎖把資源釋放為止。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。悲觀鎖的實現往往依靠數據庫本身的鎖功能實現。 實現有數據庫的鎖之類的。 只能說,各有千秋吧。 樂觀鎖適用于寫比較少的情況下,即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果經常產生沖突,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適。 悲觀鎖會造成訪問數據庫時間較長,并發性不好,特別是長事務。 樂觀鎖在現實中使用得較多。 自旋鎖和互斥鎖嘛,一直在用的,不過以前只是簡單的叫它們:鎖。原來人家有名字的啊。 wait() 曉得不?timewait()曉得不? 互斥鎖:阻塞等待 自旋鎖:等兩下就去問一聲:好了不?我很急啊!好了不?你快點啊。。。哈哈哈哈哈 自旋鎖的原理比較簡單,如果持有鎖的線程能在短時間內釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞狀態,它們只需要等一等(自旋),等到持有鎖的線程釋放鎖之后即可獲取,這樣就避免了用戶進程和內核切換的消耗。 因為自旋鎖避免了操作系統進程調度和線程切換,所以自旋鎖通常適用在時間比較短的情況下。由于這個原因,操作系統的內核經常使用自旋鎖。但是,如果長時間上鎖的話,自旋鎖會非常耗費性能,它阻止了其他線程的運行和調度。線程持有鎖的時間越長,則持有該鎖的線程將被 OS(Operating System) 調度程序中斷的風險越大。如果發生中斷情況,那么其他線程將保持旋轉狀態(反復嘗試獲取鎖),而持有該鎖的線程并不打算釋放鎖,這樣導致的是結果是無限期推遲,直到持有鎖的線程可以完成并釋放它為止。 解決上面這種情況一個很好的方式是給自旋鎖設定一個自旋時間,等時間一到立即釋放自旋鎖。適應性自旋鎖意味著自旋時間不是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖擁有的狀態來決定,基本認為一個線程上下文切換的時間是最佳的一個時間。 條件變量 條件變量提供了另一種同步的方式。互斥量通過控制對數據的訪問實現了同步,而條件變量允許根據實際的數據值來實現同步。 沒有條件變量,程序員就必須使用線程去輪詢(可能在臨界區),查看條件是否滿足。這樣比較消耗資源,因為線程連續繁忙工作。條件變量是一種可以實現這種輪詢的方式。 條件變量往往和互斥一起使用 使用條件變量的代表性順序如下: 條件變量原語 //初始化條件變量: //本人還是喜歡靜態初始化,省事兒 pthread_cont_t cont = PTHREAD_COND_INITIALIZER; //好,再看看動態初始化 int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); //參數釋義:cond:用于接收初始化成功管道條件變量 //attr:通常為NULL,且被忽略 //有初始化那肯定得有銷毀 int pthread_cond_destroy(pthread_cond_t *cond); //既然說條件變量是用來等待的,那就更要看看這等待的特殊之處了 int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex); //無條件等待 int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t mytex,const struct timespec *abstime); //計時等待 //好,加入等待喚醒大軍了,那得看看怎么去喚醒了 int pthread_cond_signal(pthread_cond_t *cptr); //喚醒一個等待該條件的線程。存在多個線程是按照其隊列入隊順序喚醒其中一個 int pthread_cond_broadcast(pthread_cond_t * cptr); //廣播,喚醒所喲與等待線程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 條件變量與互斥鎖 在服務器編程中常用的線程池,多個線程會操作同一個任務隊列,一旦發現任務隊列中有新的任務,子線程將取出任務;這里因為是多線程操作,必然會涉及到用互斥鎖保護任務隊列的情況(否則其中一個線程操作了任務隊列,取出線程到一半時,線程切換又取出相同任務)。但是互斥鎖一個明顯的缺點是它只有兩種狀態:鎖定和非鎖定。設想,每個線程為了獲取新的任務不斷得進行這樣的操作:鎖定任務隊列,檢查任務隊列是否有新的任務,取得新的任務(有新的任務)或不做任何操作(無新的任務),釋放鎖,這將是很消耗資源的。 而條件變量通過允許線程阻塞和等待另一個線程發送信號的方法彌補了互斥鎖的不足,它常和互斥鎖一起配合使用。使用時,條件變量被用來阻塞一個線程,當條件不滿足時,線程往往解開相應的互斥鎖并等待條件發生變化。一旦其他的某個線程改變了條件變量,他將通知相應的條件變量喚醒一個或多個正被此條件變量阻塞的線程。這些線程將重新鎖定互斥鎖并重新測試條件是否滿足。一般說來,條件變量被用來進行線程間的同步。對應于線程池的場景,我們可以讓線程處于等待狀態,當主線程將新的任務放入工作隊列時,發出通知(其中一個或多個),得到通知的線程重新獲得鎖,取得任務,執行相關操作。 (1)必須在互斥鎖的保護下喚醒,否則喚醒可能發生在鎖定條件變量之前,照成死鎖。 (2)喚醒阻塞在條件變量上的所有線程的順序由調度策略決定 (3)如果沒有線程被阻塞在調度隊列上,那么喚醒將沒有作用。 (4)以前不懂事兒,就喜歡廣播。由于pthread_cond_broadcast函數喚醒所有阻塞在某個條件變量上的線程,這些線程被喚醒后將再次競爭相應的互斥鎖,所以必須小心使用pthread_cond_broadcast函數。 虛假喚醒與喚醒丟失 在多核處理器下,pthread_cond_signal可能會激活多于一個線程(阻塞在條件變量上的線程)。結果是,當一個線程調用pthread_cond_signal()后,多個調用pthread_cond_wait()或pthread_cond_timedwait()的線程返回。這種效應成為”虛假喚醒”(spurious wakeup) Linux幫助里面有 為什么不去修正,性價比不高嘛。 所以通常的標準解決辦法是這樣的: 無論哪種等待方式,都必須和一個互斥量配合,以防止多個線程來打擾。 互斥鎖必須是普通鎖或適應鎖,并且在進入pthread_cond_wait之前必須由本線程加鎖。 在更新等待隊列前,mutex必須保持鎖定狀態. 在線程進入掛起,進入等待前,解鎖。(好繞啊,我已經盡力斷句了) 在條件滿足并離開pthread_cond_wait前,上鎖。以恢復它進入cont_wait之前的狀態。 為什么等待會被上鎖? 以免出現喚醒丟失問題。 這里有個大神解釋要不要看:https://stackoverflow.com/questions/4544234/calling-pthread-cond-signal-without-locking-mutex 做事做全套,源碼也給放這兒了:https://code.woboq.org/userspace/glibc/nptl/pthread_cond_wait.c.html 1 2 3 在放些咱能看懂的中文解釋:將線程加入喚醒隊列后方可解鎖。保證了線程在陷入wait后至被加入喚醒隊列這段時間內是原子的。 但這種原子性依賴一個前提條件:喚醒者在調用pthread_cond_broadcast或pthread_cond_signal喚醒等待者之前也必須對相同的mutex加鎖。 滿足上述條件后,如果一個等待事件A發生在喚醒事件B之前,那么A也同樣在B之前獲得了mutex,那A在被加入喚醒隊列之前B都無法進入喚醒調用,因此保證了B一定能夠喚醒A;試想,如果A、B之間沒有mutex來同步,雖然B在A之后發生,但是可能B喚醒時A尚未被加入到喚醒隊列,這便是所謂的喚醒丟失。 在線程未獲得相應的互斥鎖時調用pthread_cond_signal或pthread_cond_broadcast函數可能會引起喚醒丟失問題。 喚醒丟失往往會在下面的情況下發生: 一個線程調用pthread_cond_signal或pthread_cond_broadcast函數; 另一個線程正處在測試條件變量和調用pthread_cond_wait函數之間; 沒有線程正在處在阻塞等待的狀態下。 1 2 3 4 5 6 7 使用條件變量 //例子演示了使用Pthreads條件變量的幾個函數。主程序創建了三個線程,兩個線程工作,根系“count”變量。第三個線程等待count變量值達到指定的值。 #include 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 線程池 線程池 番外篇 Pthread API函數 多線程下的對象創建 對象構造要做到線程安全,就一點要求:不要暴露自己,即不要泄露this指針。 那就是做到以下幾點: 不要在構造函數中注冊任何回調 不要在構造函數中將this傳給跨線程對象 即時在構造函數最后一行也不行 1 2 3 對于第一點,如果非要回調函數才能構造,那就換二段式構造,先構造,在調用回調函數。 對于第三條,如果這個類是個基類呢?它構造完了并不是真的構造完了,還有子類等著呢。 之所以要這樣設計(把this傳給子類那另當別論),就是為了防止構造過程被打斷,構造出一個半成品。 對象的銷毀與競態條件 對象析構,在多線程里,由于競態的存在,變得撲朔迷離。 看個例子: Foo::~Foo(){ //拿鎖 //析構 //解鎖 } void Foo::update(){ //拿鎖 //數據操作 //解鎖 } extern Foo *f;//共享資源 A進程操作 delete f; f = NULL; B進程操作 if(f) { f->update(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 那這就有一個很尷尬的情況了: A在執行“析構”的時候,已經拿到了鎖,而B通過了 f 的判斷,因為那會兒指針還活著,然后被鎖卡住了。 接下來會發生什么?不知道,因為對象析構的時候把鎖也帶走了。。。(鎖屬于對象,對象析構,鎖也跑不了) 那怎么辦? 別怕,參考博客:智能指針 一個動態創建的對象,是否還有效光看指針是看不出來的指針就是指向了一塊內存而已,這塊內存上的對象如果已經被銷毀,那就根本不能訪問。 shared_ptr是引用計數型智能指針,被納入C11標準庫。shared_ptr是一個類模板,它只有一個參數,使用起來很方便。 shared_str是強引用,只要有一個指向x對象的shared_ptr存在,該對象及不會被析構。 weak_ptr是弱引用,它不控制對象的生命周期,但是它知道對象是否還存在。如果對象存在,它可以升級成為shared_ptr。 講這么多不如來個例子實在: class Observer{ private: std::vector 1 2 3 4 再聊會兒C++內存安全 C++里面可能出現的內存問題大致有這么幾個方面 緩沖區溢出 空懸指針/野指針 重復釋放 內存泄漏 不配對的new[]/delete 內存碎片 對應解決: std::vetor shared_ptr/weak_ptr scoped_ptr,只在對象析構的時候釋放一次 scoped_ptr std::vetor 資源推薦 Programing with POSIX thread(POSIX多線程程序設計) 需要私信我。 Linux 任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。