Linux多線程-概念及控制
@TOC
零、前言
本章主要講解學習linux中的線程
一、linux線程概念
1、什么是線程
概念:
在一個程序里的一個執行路線就叫做線程(thread),更準確的定義是:線程是“一個進程內部的控制序列”
一切進程至少都有一個執行線程,也就是主線程,進程由一個或者多個線程組成,即進程中可以有多個執行流
線程是進程的一個執行分支,實在進程內部運行的一個執行流,本質是在進程地址空間內運行,共享進程的進程地址空間,執行進程的一部分代碼
以整個運行視角理解:
程序運行,將代碼和數據加載到CPU上,同時系統創建對應的進程進行承擔分配系統資源,如創建task_struct結構體,構建對應的進程地址空間,頁表建立虛擬地址與物理地址的映射等等,即進程是承擔分配系統資源的基本單元
在進程中可能存在多個執行流(一定有個主執行流),也就是線程,而這些執行流都是由task_struct描述的,共享同一個進行地址空間,透過進程虛擬地址空間,可以看到進程的大部分資源,將進程資源合理分配給每個執行流,就形成了線程執行流,執行程序的部分代碼,這些執行流可以進行并發執行,由于是在進行內部運行,不用切換整個進程的上下文數據,只需切換線程的上下文數據,即線程是系統調度的基本單元
示圖:
注:在Linux系統下的CPU眼中,看到的PCB(task_struct)都要比傳統的進程更加輕量化
如何理解之前所說的’進程’:
進程是一個大的整體,包括task_struct(PCB),進程地址空間、文件、信號等,是承擔分配系統資源的基本實體,而之前所受的進程都只有一個task_struct,也就是該進程內部只有一個執行流
注意:
在Linux中,CPU只關心一個一個的獨立執行流,無論進程內部只有一個執行流還是有多個執行流,CPU都是以task_struct為單位進行調度的
Linux下并不存在真正的多線程,而是用進程模擬的。如果要支持真的線程(TCB)會提高操作系統的復雜程度。而線程的和進程的控制塊基本是類似實現的,因此Linux直接復用了進程控制塊,所以Linux中的所有執行流都叫做輕量級進程
在Linux中都沒有真正意義的線程,所以也就沒有真正意義上的線程相關的系統調用,但是Linux提供了輕量級進程相關的庫和接口,例如vfork函數和原生線程庫pthread
2、vfork函數/pthread線程庫
vfork函數原型:
pid_t vfork(void);
注意:
功能:創建子進程,但是父子共享進程地址空間
返回值:成功給父進程返回子進程的PID;給子進程返回0
示例:
#include
效果:
注:vfork() 保證子進程先運行,在它調用 exec(進程替換) 或 exit(退出進程)之后父進程才可能被調度運行;如果子進程沒有調用 exec, exit, 程序則會導致死鎖,程序是有問題的程序,沒有意義
原生線程庫pthread:
在Linux中,站在內核角度沒有真正意義上線程相關的接口,但是站在用戶角度,當用戶想創建一個線程時更期望使用thread_create這樣類似的接口,因此系統為用戶層提供了原生線程庫pthread
原生線程庫實際就是對輕量級進程的系統調用進行了封裝,在用戶層模擬實現了一套線程相關的接口
3、線程優缺點及其他分析
線程的優點:
創建一個新線程的代價要比創建一個新進程小得多
與進程之間的切換相比,線程之間的切換需要操作系統做的工作要少很多線程占用的資源要比進程少很多
能充分利用多處理器的可并行數量
在等待慢速I/O操作結束的同時,程序可執行其他的計算任務
計算密集型應用,為了能在多處理器系統上運行,將計算分解到多個線程中實現
I/O密集型應用,為了提高性能,將I/O操作重疊。線程可以同時等待不同的I/O操作(如邊下視頻邊看視頻)
注意:
計算密集型:執行流的大部分任務,主要以計算為主。比如加密解密、大數據查找等
IO密集型:執行流的大部分任務,主要以IO為主。比如刷磁盤、訪問數據庫、訪問網絡等
線程的缺點:
性能損失:一個很少被外部事件阻塞的計算密集型線程往往無法與共它線程共享同一個處理器。如果計算密集型線程的數量比可用的處理器多,那么可能會有較大的性能損失,這里的性能損失指的是增加了額外的同步和調度開銷,而可用的資源不變
健壯性降低:編寫多線程需要更全面更深入的考慮,在一個多線程程序里,因時間分配上的細微偏差或者因共享了;不該共享的變量而造成不良影響的可能性是很大的,換句話說線程之間是缺保護的
缺乏訪問控制:進程是訪問控制的基本粒度,在一個線程中調用某些OS函數會對整個進程造成影響
編程難度提高:編寫與調試一個多線程程序比單線程程序困難得多
線程異常:
單個線程如果出現除零,野指針問題導致線程崩潰,進程也會隨著崩潰
線程是進程的執行分支,線程出異常,就類似進程出異常,進而觸發信號機制,終止進程,進程終止,該進程內的所有線程也就隨即退出
線程用途:
合理的使用多線程,能提高CPU密集型程序的執行效率
合理的使用多線程,能提高IO密集型程序的用戶體驗(如生活中我們一邊寫代碼一邊下載開發工具,就是多線程運行的一種表現)
二、Linux進程VS線程
1、進程和線程
概念:
進程是資源分配的基本單位
線程是調度的基本單位
線程共享進程數據,但也有線程自己獨有的數據:
線程ID
一組寄存器中線程自己的上下文數據
棧
errno
信號屏蔽字(handler方法是共享的)
調度優先級
線程中共享的數據:
代碼段和數據段
文件描述符表
每種信號的處理方式
當前工作目錄
用戶id和組id
注:進程的多個線程共享同一地址空間,因此Text Segment、Data Segment都是共享的,如果定義一個函數,在各線程中都可以調用,如果定義一個全局變量,在各線程中都可以訪問到
進程和線程的關系圖:
三、Linux線程控制
1、POSIX線程庫
pthread線程庫是應用層的原生線程庫:
應用層指的是這個線程庫并不是系統接口直接提供的,而是由第三方提供的
原生指的是大部分Linux系統都會默認帶上該線程庫
與線程有關的函數構成了一個完整的系列,絕大多數函數的名字都是以“pthread_”打頭的
要使用這些函數庫,要通過引入頭文件
鏈接這些線程函數庫時,要使用編譯器命令的“-lpthread”選項
錯誤檢查:
傳統的一些函數是,成功返回0,失敗返回-1,并且對全局變量errno賦值以指示錯誤
pthreads函數出錯時不會設置全局變量errno(而大部分POSIX函數會這樣做),而是將錯誤代碼通過返回值返回
pthreads同樣也提供了線程內的errno變量,以支持其他使用errno的代碼。對于pthreads函數的錯誤,建議通過返回值來判定,因為讀取返回值要比讀取線程內的errno變量的開銷更小
2、線程創建
pthread_create函數原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
解釋:
功能:創建一個新的線程
參數:thread:輸出型參數,返回獲取線程ID;attr:設置線程的屬性,attr為NULL表示使用默認屬性;start_routine:是個函數地址,線程啟動后要執行的函數,該函數返回值為void *,參數為void *;arg:傳給線程啟動函數的參數
返回值:成功返回0;失敗返回錯誤碼
注意:
主線程調用pthread_create函數創建一個新線程,此后新線程就會跑去執行參入的函數,而主線程則繼續往下執行
對于執行函數來說,參數和返回值的類型都是void *,void *是一個通用的類型,可以傳入或者返回數據和其他類型的指針,從而傳入和帶出多樣的類型和數據
示例:
mypthread.c: #include
效果:
查看線程信息:ps -aL
注意:
默認情況下,ps命令不帶-L,看到的就是一個個的進程;帶-L就可以查看到每個進程內的多個輕量級進程
在Linux中,應用層的線程與內核的LWP是一一對應的,實際上操作系統調度的時候采用的是LWP,而并非PID,只不過我們之前接觸到的都是單線程進程,其PID和LWP是相等的
3、線程ID及線程地址空間布局
概念:
pthread_ create函數會產生一個線程ID,存放在第一個參數指向的地址中。該線程ID和前面說的線程ID不是一回事
前面講的線程ID(LWP)屬于進程調度的范疇。因為線程是輕量級進程,是操作系統調度器的最小單位,所以需要一個數值來唯一表示該線程
pthread_ create函數第一個參數指向一個虛擬內存單元,該內存單元的地址即為新創建線程的線程ID,屬于NPTL線程庫的范疇。線程庫的后續操作,就是根據該線程ID來操作線程的
在Linux系統層面有LWP與線程對應,但是Linux是用輕量級進程模擬的線程,而對于用戶來說,并不會關心底層實現,從用戶角度來說,他們也需要知道線程的信息,狀態以及操作線程,由此在共享區中還相應的構建了TCB(線程控制塊),便于用戶操作線程,在用戶區進行維護
pthread_ self函數原型:
pthread_t pthread_self(void);
功能:獲得線程自身的ID
注:對于Linux目前實現的NPTL實現而言,pthread_t類型的線程ID,本質就是一個進程地址空間上的一個地址
示圖:
注:主線程并不使用動態庫里的線程棧,而是使用進程里的棧
4、線程終止
終止線程的三種方法:
從線程函數return
線程可以調用pthread_ exit終止自己
線程可以調用pthread_ cancel終止同一進程中的另一個線程或者自己
注:在主線程使用return,以及在線程中使用exit都會終止整個進程
pthread_exit函數原型:
void pthread_exit(void *value_ptr);
解釋:
功能:線程終止
參數:value_ptr線程退出傳出的數據(不要指向一個局部變量)
返回值:無返回值,跟進程一樣,線程結束的時候無法返回到它自身
注:pthread_exit或者return返回的指針所指向的內存單元必須是全局的或者是用malloc分配的,不能在線程函數的棧上分配,因為當其它線程得到這個返回指針時線程函數已經退出了
pthread_cancel函數原型:
int pthread_cancel(pthread_t thread);
解釋:
功能:取消一個執行中的線程
參數:thread表示要操作的線程的ID
返回值:成功返回0;失敗返回錯誤碼
注:pthread_cancel函數具有一定的延時性,并不會立即被處理,不建議當線程立即被創建后立即進行cancel取消(線程創建,并不會立即被調度);也不建議在線程退出前執行線程cancel取消(線程可能在取消之前就已經退出了);建議在線程執行中進行cancel取消線程
示例:
#include
效果:
5、線程等待
為什么需要線程等待:
已經退出的線程,其空間沒有被釋放,仍然在進程的地址空間內,創建新的線程不會復用剛才退出線程的地址空間,如果主線程不對新線程進行等待,那么這個新線程的資源也是不會被回收的。如果不等待會產生內存泄漏
線程是用來執行分配的任務的,如果主線程想知道任務完成的怎么樣,那么就有必要對線程進行等待,獲取線程退出的信息
pthread_join函數原型:
int pthread_join(pthread_t thread, void **value_ptr);
解釋:
功能:等待線程結束
參數:thread:指定等待線程的ID;value_ptr:輸出型參數,用來獲取指向線程的返回值
返回值:成功返回0;失敗返回錯誤碼
注意:
調用該函數的線程將掛起等待,直到id為thread的線程終止
這里獲取的線程退出信息并沒有終止信號信息,而終止信號信息是對于整個進程來說的,如果線程收到信號崩潰也會導致整個進程也崩潰
thread線程以不同的方法終止,通過pthread_join得到的終止狀態是不同的
終止獲取的狀態情況:
如果thread線程通過return返回,value_ ptr所指向的單元里存放的是thread線程函數的返回值
如果thread線程被別的線程調用pthread_ cancel異常終掉,value_ ptr所指向的單元里存放的是常數PTHREAD_ CANCELED
如果thread線程是自己調用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數
如果對thread線程的終止狀態不感興趣,可以傳NULL給value_ ptr參數
示圖:
示例:
#include
效果:
6、線程分離
概念:
默認情況下,新創建的線程是joinable的,線程退出后,需要對其進行pthread_join操作,否則無法釋放資源,從而造成系統泄漏
如果不關心線程的返回值,join是一種負擔,這個時候,我們可以告訴系統,當線程退出時,自動釋放線程資源
pthread_detach函數原型:
int pthread_detach(pthread_t thread);
注意:
可以是線程組內其他線程對目標線程進行分離,也可以是線程自己分離: pthread_detach(pthread_self());
joinable和分離是沖突的,一個線程不能既是joinable又是分離的
線程的分離也是具有一定延時性,分離之后如果再進行等待那么得到返回的結果是未定義的
線程分離后只是回收的時候自動進行回收,如果主線程先退出,那么整個進程也會退出;如果分離的線程執行崩潰,同樣的整個進行也會崩潰
示例:
#include
效果:
Linux 任務調度 多線程
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。