(1)多線程多進程的區別
維度 |
多進程 |
多線程 |
總結 |
數據共享、同步 |
數據是分開的:共享復雜,需要用IPC;同步簡單 |
多線程共享進程數據:共享簡單;同步復雜 |
各有優勢 |
內存、CPU |
占用內存多,切換復雜,CPU利用率低 |
占用內存少,切換簡單,CPU利用率高 |
線程占優 |
創建銷毀、切換 |
創建銷毀、切換復雜,速度慢 |
創建銷毀、切換簡單,速度快 |
線程占優 |
編程調試 |
編程簡單,調試簡單 |
編程復雜,調試復雜 |
進程占優 |
可靠性 |
進程間不會相互影響 |
一個線程掛掉將導致整個進程掛掉 |
進程占優 |
分布式 |
適應於多核、多機分布 ;如果一台機器不夠,擴展到多台機器比較簡單 |
適應於多核分布 |
進程占優 |
然后我們來看下線程和進程間的比較
子進程繼承父進程的屬性: |
子線程繼承主線程的屬性: |
實際用戶ID,實際組ID,有效用戶ID,有效組ID; 附加組ID; 進程組ID; 會話ID; 控制終端; 設置用戶ID標志和設置組ID標志; 當前工作目錄; 根目錄; 文件模式創建屏蔽字(umask); 信號屏蔽和安排; 針對任一打開文件描述符的在執行時關閉(close-on-exec)標志; 環境; 連接的共享存儲段; 存儲映射; 資源限制; |
進程中的所有信息對該進程的所有線程都是共享的; 可執行的程序文本; 程序的全局內存; 堆內存; 棧; 文件描述符; 信號的處理是進程中所有線程共享的(注意:如果信號的默認處理是終止該進程那么即是把信號傳給某個線程也一樣會將進程殺掉);
|
父子進程之間的區別: |
子線程特有的: |
fork的返回值(=0子進程); 進程ID不同; 兩個進程具有不同的父進程ID; 子進程的tms_utime,tms_stime,tms_cutime以及tms_ustime均被設置為0; 不繼承父進程設置的文件鎖; 子進程的未處理鬧鍾被清除; 子進程的未處理信號集設置為空集; |
線程ID; 一組寄存器值; 棧; 調度優先級和策略; 信號屏蔽字; errno變量; 線程私有數據; |
1)需要頻繁創建銷毀的優先用線程。
實例:web服務器。來一個建立一個線程,斷了就銷毀線程。要是用進程,創建和銷毀的代價是很難承受的。
2)需要進行大量計算的優先使用線程。
所謂大量計算,當然就是要消耗很多cpu,切換頻繁了,這種情況先線程是最合適的。
實例:圖像處理、算法處理
3)強相關的處理用線程,弱相關的處理用進程。
什么叫強相關、弱相關?理論上很難定義,給個簡單的例子就明白了。
一般的server需要完成如下任務:消息收發和消息處理。消息收發和消息處理就是弱相關的任務,而消息處理里面可能又分為消息解碼、業務處理,這兩個任務相對來說相關性就要強多了。因此消息收發和消息處理可以分進程設計,消息解碼和業務處理可以分線程設計。
4)可能擴展到多機分布的用進程,多核分布的用線程。
5)都滿足需求的情況下,用你最熟悉、最拿手的方式。
至於”數據共享、同步“、“編程、調試”、“可靠性”這幾個維度的所謂的“復雜、簡單”應該怎么取舍,只能說:沒有明確的選擇方法。一般有一個選擇原則:如果多進程和多線程都能夠滿足要求,那么選擇你最熟悉、最拿手的那個。
(2)多線程知識點
線程同步方式:互斥鎖、條件變量、信號量,讀寫鎖
1>互斥鎖:一個時間內只准一個線程進入關鍵代碼
相關接口:
pthread_mutex_init();
pthread_mutex_lock()
pthread_mutex_trylock()
pthread_mutex_unlock();
pthread_mutex_destory()(此時鎖必須為unlock狀態)
2>條件變量:利用線程之間共享一個全局變量是實現同步;基本操作有:觸發條件(當條件為true時),等待條件,掛起線程直到其他線程觸發條件
相關接口:
pthread_cond_init()
pthread_cond_wait()
pthread_cond_timewait() //計時等待
無論哪種等待都要配合一個鎖使用,防止多線程同時請求競爭條件
pthread_cond_singal() //激活一個線程
pthread_cond_broadcast()//激活所有線程
pthread_cond_destory() //沒有線程在等待時銷毀,否則返回EBUSY
一般用法:
pthread_mutex_lock();
pthread_cond_wait();
pthread_mutex_unlock
猜測pthread_cond_wait源碼:
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex)
{
if(沒有條件變量)
{
(1)pthread_mutex_unlock(mutex);
(2)阻塞當前線程,等待信號(當前應該是類似於中斷觸發的等待方式,而不是軟件輪詢)
(3)pthread_mutex_lock()
}
......
}
3> 信號量:跟進程的信號量一樣
相關接口:
頭文件<semaphore.h>
sem_init(sem_t *sem,int pshared,int value) ; // pshared=0(linux下只能為0),為進程局部信號量
sem_post(sem_t *sem) //信號量加1 v操作
sem_wait(sem_t *sem) //信號量減1 P操作
sem_destroy(sem_t *sem)
4>讀寫鎖:多個讀鎖可以共享一個臨界區,寫鎖與寫鎖互斥,寫鎖與讀鎖互斥
相關接口:
pthread_rwlock_init()
pthread_rwlock_rdlock()
pthread_rwlock_wrlock()
pthread_rwlock_unlock()
pthread_rwlock_tryrdlock()
pthread_rwlock_trywrlock()
pthread_rwlock_timerdlock()
pthread_rwlock_timewrlock()
pthread_rwlock_destroy()
擴展:自旋鎖
(1)請求鎖,未請求到,就一直忙等待,直到請求到這個鎖,不進行上下文切換
(2)有可能造成死鎖:遞歸調用,臨界區引起阻塞
(3)使用場景:鎖持有時間較短的
(4)自旋鎖與linux內核進程調度的關系
自旋鎖保護的臨界區工作在非搶占式的狀態,即使獲取不到鎖,“自旋”狀態也是禁止搶占的,在spin_unlock時重新開啟搶占
總結:
1>單CPU非搶占式:自旋鎖編譯時被忽略(因為不會發生進程切換,只有一個進程或線程處於臨界區,自旋鎖沒有用)
2>單CPU搶占式式:自旋鎖僅僅當做一個設置搶占的開關(因為單CPU下不會涉及到並發訪問,設置禁止搶占就可以保證臨界區被唯一擁有)
3>多CPU:防止多進程並發訪問,防止內核搶占造成競爭
線程安全:原子操作、鎖、可重入、防止過度優化
原子操作:保證指令原子的指向不被打斷,Linux系統提供了一些常用操作的原子指令,<atuomic.h>,包括原子整數操作和原子位操作,必須為atuomic_t類型的整數(32位的整數,24位數據,8bits的鎖)
鎖:對臨界區代碼進行互斥訪問
可重入:也就是可以被打斷,再次進入時沒有什么影響,這意味着它除了使用自己棧上的變量以外不依賴於任何環境(包括static)
防止過度優化:我們可以使用volatile關鍵字試圖阻止過度優化,它可以做兩件事:第一,阻止編譯器為了提高速度將一個變量緩存到寄存器而不寫回;第二,阻止編譯器調整操作volatile變量的指令順序。
無鎖化編程:
針對計數器,可以使用原子加
只有一個生產者和一個消費者,那么就可以做到免鎖訪問環形緩沖區(Ring Buffer)
RCU(Read-Copy-Update):新舊副本切換機制,對於舊副本可以采取延遲釋放的做法。原理:對於被RCU保護的共享數據結構,讀者不需要獲取任何鎖就可以訪問他,但是寫着在訪問它時首先拷貝一個副本,然后對副本進行修改,最后使用一個回調(callback)機制在適當的時機把指向原來數據的指針重新指向新的被修改的數據。這個時機就是所有引用該數據的CPU都退出共享數據結構的操作。寫者在訪問共享數據結構時不需要跟讀者競爭任何鎖,只有在多個寫者的情況下才要求同步。RCU技術的核心就是寫操作分為寫和更新兩步
CAS(Compare-and-Swap):
CAS 原語負責將某處內存地址的值(1 個字節)與一個期望值進行比較,如果相等,則將該內存地址處的值替換為新值,CAS 操作偽碼描述如下:
Bool CAS(T* addr, T expected, T newValue) { if( *addr == expected ) { *addr = newValue; return true; } else return false; }
(3)多進程知識點
多進程通信:管道,命名管道,消息隊列,共享內存、套接字