線程安全 (這些接口都是C庫實作的)
黃牛搶票(黃牛---線程, 票 --- 臨界資源) 兩個黃牛搶到同一張票,
1. 線程安全指的是多個線程同時運作通路臨界資源, 不會導緻程式的結果産生二義性,
臨界資源: 在同一時刻, 該資源隻能被一個執行流所通路, 涉及臨界資源的區域 --> 臨界區
通路: 在臨界區當中對臨界資源進行非原子操作,意味着可以被打斷
(原子操作是一步完成的,目前操作隻有兩個結果, 要不完成, 要不未開始)
2. 如何保證線程安全
互斥: 保證在同一時刻隻有一個執行流通路臨界資源
互斥鎖: (這個鎖是在堆上開辟的, 而這個堆也是在PCB中的 屬于task_struct結構體中 虛拟位址空間的堆)
!!!!!! 一定要在建立線程之前去初始化互斥鎖, 這樣就可以線上程内部使用互斥鎖了, 當線程退出後, 需要釋放互斥鎖資源
!!!!!! 加鎖一定是要在通路臨界資源之前加的, 拿到鎖資源之後, 再去通路臨界資源
1. 使用互斥鎖來保證互斥屬性 (他維護他自己的計數器)
2. 互斥鎖本質上是一個計數器, 但是這個計數器隻有兩個取值, 一個為0,一個為1,
0代表: 無法擷取互斥鎖
1代表: 可以獲得互斥鎖
3 在通路臨界資源前, 先擷取互斥鎖
如果能夠擷取到互斥鎖, 表示目前資源可以去通路
如果不能擷取到互斥鎖, 表示目前資源不可以去通路, 也就是意味着計數器當中的值為0, 意味着已經有一個線程正在通路臨界資源
4. 使用流程:
1. 初始化互斥鎖
2. 加鎖(将計數器當中的值變成0)
如果計數器為1, 可以正常加鎖
如果計數器為0, 不可以正常加鎖, 目前想要加鎖的執行流被阻塞
3. 通路臨界資源
4. 解鎖(将計數器當中的值變成1)
5. 操作接口
1. 定義互斥鎖
pthread_mutex_t:互斥鎖變量類型
2. 初始化互斥鎖
int pthread_mutex_init(pthread_mutex_t* mutex, pthread_mutexattr_t* attr) (動态初始化)
mutex: 互斥鎖變量, 要初始化哪一個互斥鎖變量, 一般情況下, 定義互斥鎖對象, 傳入互斥鎖對象的位址
attr: 互斥鎖的屬性, 一般情況下, 我們直接置為NULL, 采用預設屬性
課後調研: 為什麼可以直接這樣指派就初始化了????????????????? 檢視源碼 檢視定義
還有一種初始化方式為指派初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER(也叫靜态初始化)
3. 加鎖
int pthread_mutex_lock(pthread_mutex_t* mutex) ----> 阻塞加鎖操作
mutex: 要對哪一個互斥鎖進行加鎖, 傳遞互斥鎖對象的位址即可
該接口為阻塞接口, 如果加鎖成功則傳回, 如果加鎖失敗, 直到加鎖成功
這是一個帶有逾時的加鎖接口
int pthread_mutex_timedlock(pthread_mutex_t* mutex, const struct timespec* abs_timeout)
mutex: 要對哪一個互斥鎖進行加鎖, 傳遞互斥鎖對象的位址即可
abs_timeout: 加鎖逾時時間, 在加鎖的時候, 如果超過逾時時間還沒有加上鎖, 則直接傳回, 會報錯, 報錯TIMEOUT, 不會進行阻塞等待
struct timespec: 有兩成員, 第一個變量為秒級, 第二個為納秒
int pthread_mutex_trylock(pthread_mutex_t* mutex) ----> 非阻塞加鎖操作
mutex: 傳入互斥鎖的位址, 來進行加鎖操作
如果計數器值為1, 意味着可以加鎖, 加鎖操作後, 對計數器當中隻從1改變成為0
如果計數器值為0, 一位這不可以加鎖, 該接口直接傳回, 不進行阻塞等待, 傳回EBUSY, 表示拿不到加鎖資源
4. 解鎖操作:
int pthread_mutex_unlock(pthread_mutex_t* mutex)
不管使用pthread_mutex_lock/pthread_mutex_trylock/pthread_mutex_timedlock三種哪種接口加鎖, 都可以使用unlock接口都可以解鎖
如果不解鎖, 導緻阻塞等待鎖資源 , 不解鎖就會使執行流陷入阻塞, 在二次對互斥鎖加鎖時, owner線程也進入等待
thread apply all bt: 調試的時候使用這個指令 可以檢視所有線程的調用堆棧
如果不釋放互斥鎖, 則會阻塞其他線程, 我們稱之為死鎖
互斥鎖隻能被自己線程(誰拿到鎖資源)釋放, 别的線程都釋放不了
什麼時候解鎖呢???????????????????????????????????????????????????????
若隻在入口函數退出是加入解鎖, 導緻線程用完互斥鎖之後, 程式執行後, 第一個工作線程把鎖拿走了, 未解鎖, 且退出了(類似偷走了), 其他線程進行不了解鎖操作, 計數器的值依舊為0, 導緻阻塞
是以應在所有可能退出的地方都要加上解鎖操作,
5. 銷毀互斥鎖變量
int pthread_mutex_destory(pthread_mutex_t* mutex)
CPU是作業系統去管理的, 其他程式分時間在CPU上運作
問題: 對于加鎖操作, 将計數器當中的值從1變成0, 為什麼是原子操作?
這個互斥鎖的計數器本身就是C庫在維護的, 計數器本身也是一個變量, 這個變量儲存了1/0; 根據馮諾依曼體系結構, 記憶體和CPU, 寄存器的關系, 進行加鎖時 需要CPU去判斷是否可以加鎖, 計數器(mutex_val)的值儲存在記憶體中, 不管計數器的值是怎樣, 隻有加鎖都給寄存器中放一個0, 加鎖時交換寄存器與記憶體的值, xchgb(彙編中的指令), 這個交換操作是原子性的, 在交換結束後, 接下來隻需要去
判斷寄存器中的值是否為1, 來判斷是否可以加鎖,
如果寄存器中的值為1, 則表示之前記憶體中計數器的值為1, 表示可以加鎖,
如果為0, 則表示之前記憶體中計數器的值為0, 表示不可以加鎖;
主要是由于這個交換操作保證了原子性.
上鎖: 第一步是将寄存器的值變為0, 第二步将寄存器當中的值與記憶體的值交換, 接着判斷寄存器當中的值是否大于0, 如果大于0 , 直接傳回, 說明lock這個函數傳回了, 當等于0時 進行上鎖, 當寄存器中的值為0時, 上鎖成功
解鎖時, 将記憶體中計數器的值變為1 (每句彙編代碼都保證了原子性, 但是我們所寫的指令則可能是多條彙編代碼, 而被排程的時候是一條指令還執行完, 傳回時從上次中斷的彙編代碼的下一條代碼開始 恢複現場)
死鎖:
1. 什麼是死鎖?
程式當中有喲個執行流沒有釋放鎖資源, 會導緻其他想要擷取該所的執行流陷入阻塞等待, 這種情況被稱為死鎖
程式當中每一個執行流都占有一把互斥鎖, 但是由于各個執行流在占有互斥鎖(已經擷取)的情況下, 還想申請對方的鎖(其他執行流所占有的互斥鎖), 這種也會導緻死鎖 ------ 吃着碗裡的,看着鍋裡的, 誰申請誰阻塞
條件變量 --> 同步: 保證程式對臨界資源通路的合理性
信号量 --> 既能保證同步 也能保證互斥