文章目錄
- 線程控制
- 線程共享進程地址空間中的所有數據
- 線程會瓜分進程的時間片
- 線程相關庫函數
- 庫函數:pthread_create
- 庫函數:pthread_self
- 庫函數:pthread_join
- 庫函數:pthread_exit
- 庫函數:pthread_cancel[盡量少用]
- 庫函數:pthread_detach
- 為什么Linux線程相關的函數是庫函數,而不是系統調用?
- 線程的狀態和回收
- 線程終止
- 線程分離
- 線程庫的理解
- 線程id
- 線程TCB的存放和內容
- TCB的存儲
- TCB中的內容
- 線程的棧
- 線程局部存儲
線程控制
線程共享進程地址空間中的所有數據
全局區,代碼區,堆區,共享區,棧區都是共享的
沒錯棧區也是共享的
只要線程a能拿到線程b函數中的棧區變量的地址,就可以對它進行修改和訪問
而且修改之后,線程b看到的也是改變了之后的【但是強烈不建議這么做
】
線程會瓜分進程的時間片
即:
一個進程的時間片是10ns
它一共有5個線程,那么每個線程的時間片就是2ns
為什么呢?
操作系統進行調度時,是以線程為單位的
而且承擔資源分配的基本實體是進程,進程是資源的容器,線程瓜分進程資源,而時間片也是資源!!!
所以,必須給每個線程分時間片,這樣操作系統調度線程時,才知道線程什么時候時間片耗盡
而且:
如果線程的時間片和它所屬的進程一樣長,那么一個進程就可以通過創建更多線程,來延長自己占用CPU的時間
這顯然對其他進程不公平
這和分時操作系統的公平調度理念不符
線程相關庫函數
庫函數:pthread_create
-
頭文件:
pthread.h
-
返回值:int
①成功,就返回0
②失敗,就返回錯誤碼 -
參數表:
-
①
pthread_t *thread
:輸出型參數,用于獲取線程tid,本質也是一個整型 -
②
const pthread_attr_t *attr
:線程屬性 -
③
(void*)(*p)(void*)
:指向給這個線程分配的函數的函數指針 -
④
void*arg
:傳給分配給線程的函數的參數
為什么是void * 類型?
就是為了支持接收任意類型的參數,變量,數組,對象都可以傳遞過去
這樣就可以一次傳遞多個信息,也可以更好地支持C和C++混合編寫
我們可以定義一個任務類,里面存儲一些基本信息和分配給這個新線程的任務函數
即設計一個任務類,里面的成員變量就是任務函數,以及任務函數對應的參
-
-
作用:創建一個線程
庫函數:pthread_self
-
頭文件:
pthread.h
-
返回值:
pthread_t
①成功,該線程的tid
②失敗,就返回-1 -
作用:獲取唯一標識一個線程的tid
庫函數:pthread_join
-
頭文件:
pthread.h
-
返回值:int
①成功,0
②失敗,就返回錯誤碼 -
參數表:
-
①
pthread_t thread
:要等待的線程的tid -
②
void**retval
:因為分配給新線程的函數的返回值為void*
類型的,所以以輸出型參數的方式獲取這個返回值就得是void**
類型的
-
-
注意:
pthread_join是阻塞等待的從線程的
一般是主線程等待從線程
庫函數:pthread_exit
-
頭文件:
pthread.h
-
返回值:void
-
參數表:
void*retval
:線程的退出信息 -
作用:讓調用這個函數的線程退出
庫函數:pthread_cancel[盡量少用]
-
頭文件:
pthread.h
-
返回值:int
①成功,0
②失敗,就返回錯誤碼 -
參數表:
pthread_t thread
:要取消的線程的tid -
注意:
- ①一個線程能被取消的前提是,這個線程已經啟動了
- ②被取消的線程也必須被回收(不然會內存泄露),而且退出碼必定是-1
庫函數:pthread_detach
-
頭文件:
pthread.h
-
參數表:
pthread_t thread
:要設置成要把被等待狀態設置成detach的線程的tid -
作用:
把指定線程的被等待狀態設置成detach
為什么Linux線程相關的函數是庫函數,而不是系統調用?
因為在Linux中沒有給線程先描述再組織
Linux內核中的線程是使用LWP模擬實現的
所以
Linux內核就只給我們提供了創建LWP的系統調用接口(即系統調用clone)操作線程的各種系統調用通通沒有
用戶不愿意直接去使用創建LWP的系統調用,因為使用這個系統調用還得去了解Linux的LWP等相關知識,成本太高了
所以
Linux就讓庫封裝了LWP相關的系統調用得到了給用戶提供了包括線程的各種操作的的庫[庫的名字就叫pthread]
封裝成庫之后,用戶就只需要知道線程的相關概念就可以輕松使用庫函數了
所以
Linux中,如果使用了線程相關的函數,那么編譯時都要鏈接pthread動態庫,pthread是Linux實現的自帶的庫,是原生線程庫
所以:
其他任何語言想要在Linux平臺上支持線程,就必須封裝Linux的pthread庫
線程的狀態和回收
Linux中的線程就是用LWP(task_struct)模擬實現的
所以一個線程對應一個PCB
而PCB里面就存儲了狀態信息
所以線程的狀態管理,完全可以復用進程的狀態管理
從線程也需要被主線程等待和回收(使用pthread_join)
為什么?
-
①因為線程的退出信息,在線程庫中它自己的TCB中維護著,而TCB關聯內核的LWP的生命周期
-
②因為主線程也要知道其他線程把任務執行地怎么樣
所以如果不等待,即使線程退出了,操作系統也不敢釋放它的LWP,就會出現與進程類似的僵尸問題
線程終止
會讓線程終止的方法一共3種
-
①分配給線程的函數
return
,該線程會退出 -
②線程調用了
pthread_exit
,該線程就會退出 -
③其他線程(一般是主線程)調用
pthread_cancel
取消線程
線程分離
主線程也可以不等待新線程,自己干自己的事
但是線程并不提供非阻塞等待
而是通過修改新線程的被等待狀態,來做到
線程有兩種被等待的狀態
-
①
joinable
:線程需要被join等待和回收(新線程默認是joined狀態) -
②
detach
:線程分離(這種狀態的線程執行完了就直接退出,即使主線程調用pthread_join
專門去等待分離狀態的線程,也會因為檢測到它是分離狀態,主線程直接返回,不會阻塞)
此時要注意一點:
如果線程a和主線程分離了,如果線程a還在運行,主線程就退出了
這個時候可能:[不同的線程庫實現不一樣]
- ①因為主線程退出,進程就直接也退出了
- ②主線程退出,但是進程沒退出
注意:
多執行流時,盡可能保證主執行流最后退出=
線程庫的理解
線程id
我們使用的pthread庫里面的接口的時候,使用的事線程id來操縱線程
但是線程id并不是Linux內核中的LWP,而是線程庫pthread自己維護的
線程id的本質是地址,是線程庫維護的這個線程對應的結構體(tcb)變量的起始地址
創建線程的時候,不僅Linux內核中會創建一個LWP描述線程的內核級屬性,線程庫pthread中也會創建一個TCB結構體變量來維護線程的用戶級屬性
即線程庫中,會對線程進行先描述,再組織
為什么?
-
①LWP是描述輕量級進程的,但是用戶使用的是線程,所以用戶需要的是線程的屬性,而不是輕量級進程的屬性
即LWP并不能非常好地描述用戶所需的線程的屬性
(比如:LWP中沒有維護這個線程的棧大小,而線程庫TCB中維護了) -
②為了解藕
因為用戶使用的是線程庫,線程庫創建出來的線程是用戶級線程
用戶級線程不宜和內核級線程共用結構體和數據結構,這樣耦合度太高,而且還要使用系統調用陷入內核
線程庫哪里來的空間對線程先描述再組織?
線程庫維護進程的線程使用的空間是進程自己申請的物理內存(即使用進程的共享區內存來存儲
)
因為這樣描述線程屬性的結構體變量才有虛擬地址,執行流執行線程的庫函數的時候,才能找到并訪問到對應的數據
所以就可以做到:
不同的進程,使用同一個線程庫,但線程庫中維護的線程是不一樣的
但是線程庫的代碼區的代碼是一樣的
線程TCB的存放和內容
TCB的存儲
線程庫中TCB的存儲是在共享區中開辟一塊內存,把所有TCB以及對應的連續存儲在一起的
TCB中的內容
- ①對應的輕量級進程的LWP
- ②對應進程的pid
- ③void*result:分配給自己這個線程的函數的返回值
- ④用戶分配給自己這個線程的入口函數的地址,以及它對應的參數
- ⑤自己這個線程對應的棧的起始虛擬地址,以及棧的大小
- ⑥線程對應的線程局部存儲的起始地址,及其大小
- ⑦標記線程是否分離的bool類型的變量
線程的棧
線程的棧是獨立的,分兩種
-
①進程地址空間中所謂的棧區,其實是專門給主線程用的棧區,大小是不固定的,可以擴容(
當然也受到mm_struct的區域劃分限制
) -
②新線程的棧區是在創建新線程的同時,使用mmap在共享區開辟的一塊的大小固定(一般默認是8MB,如果用滿了8MB還用就會棧溢出)的內存
所以才說線程的棧是獨立的
線程局部存儲
一個全局變量b本來是被所有線程共享的,所有線程訪問這個的全局變量b時,訪問的虛擬地址是同一個
如果定義時,給全局變量b前面加一個__thread
那么這個全局變量b就不是被所有線程共享的了,不同線程訪問這個變量b時,訪問的虛擬地址不同
這是怎么做到的?
如果一個變量a定義時加了__thread,編譯器編譯的時候,如果創建了新線程,就會在這個線程的線程局部存儲中開辟空間存儲一份這個變量a
以后這個線程訪問這個變量a的時候,就只訪問自己線程局部存儲中的變量a
注意:
__thread只能修飾內置類型的變量
線程局部存儲的作用:
-
①不同線程對應一個獨立的錯誤碼,比如C標準庫提供的errno
-
②緩存線程屬性等訪問頻繁的數據,提高訪問速度
比如:
可以加__thread
定義一個全局的線程id,在對應線程里面初始化之后
這樣以后使用線程id,就不需要在調用pthread_self
,直接用它,不同的線程看到的也是自己的id