中山網站建設技術如何做seo優(yōu)化
?
?13.?常見鎖概念
(一)了解死鎖
死鎖是指在一組進程中的各個進程均占有不會釋放的資源,但因互相申請被其他進程占有的,且不釋放的資源,而處于的一種永久等待狀態(tài)
(二)死鎖四個必要條件
- 互斥條件:一個資源每次只能被一個執(zhí)行流使用
- 請求與保持條件:一個執(zhí)行流因請求資源而阻塞時,對已獲得的資源保持不放
- 不剝奪條件:一個執(zhí)行流已獲得的資源,在末使用完之前,不能強行剝奪
- 循環(huán)等待條件:若干執(zhí)行流之間形成一種頭尾相接的循環(huán)等待資源的關系
(二)避免死鎖
- 破壞死鎖的四個必要條件
- 加鎖順序一致 (不是交錯申請不同的鎖資源)
- 避免鎖未釋放的場景
- 資源一次性分配
14. linux線程同步
(一)條件變量
當一個線程互斥地訪問某個變量時,它可能發(fā)現在其它線程改變狀態(tài)之前,它什么也做不了,為了使搶奪鎖資源更公正(防止饑餓問題),就需要把所有等待鎖資源的線程放入一個等待隊列,直到線程被喚醒
。這種情況就需要用到條件變量
注意:
條件變量必須依賴鎖
(二)同步概念
同步:在保證數據安全的前提下,讓線程能夠按照某種特定的順序訪問臨界資源,從而有效避免饑餓問題,叫做同步
(三)條件變量相關函數
條件變量函數 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
參數:
cond:要初始化的條件變量
attr:條件變量的屬性,一般設置成NULL
銷毀
int pthread_cond_destroy(pthread_cond_t *cond)
參數:要銷毀的條件變量函數
等待條件滿足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
參數:
cond:要在這個條件變量上等待
mutex:互斥量
注意:
參數中有互斥量,是因為實現同步與互斥,需要先加鎖,再判斷(判斷也是訪問臨界資源,和加鎖保護線程安全對應)是否進入等待隊列休眠,而這個函數里面有互斥量是為了把鎖資源釋放,接下來訪問臨界資源的順序是按照隊列里面的順序進行的(前提是線程被喚醒)
大概代碼思路
pthread_mutex_lock(&mutex); // 加鎖
pthread_cond_wait(&cond); //放入等待隊列
pthread_mutex_unlock(&mutex); //解鎖
注意:
- 喚醒可以由其它線程去喚醒,如主線程
- 三個函數的順序不要更換(解鎖和等待都不是原子操作)
喚醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
喚醒隊列中的所有線程
int pthread_cond_signal(pthread_cond_t *cond);
按順序喚醒隊列中的一個線程
注意:
- 喚醒之后的線程需要申請鎖資源
- 喚醒的時候,可能出現喚醒錯誤的情況。比如在生產者消費者模型中(多生產者多消費者容易出現這種錯誤),喚醒了多個生成者,并且消費者此時也是屬于喚醒狀態(tài),導致生產者和生產者之間,消費者和生產者之間競爭同一把鎖,鎖最后的分配,可能導致臨界資源訪問條件不滿足(喚醒之后拿到鎖資源的線程,有可能這個時候的訪問條件不滿足,但依然會執(zhí)行后面的代碼),出現錯誤
正確代碼思路
pthread_mutex_lock(&mutex); // 加鎖
while(... ...) //判斷訪問資源的條件是否成立
{
pthread_cond_wait(&cond); //放入等待隊列
}
pthread_mutex_unlock(&mutex); //解鎖
注意:
判斷用while,是為了保證,線程被喚醒(出了等待隊列),并且競爭到了鎖資源后,再一次判斷資源條件是否成立,才能決定后面的代碼能否執(zhí)行
15. 生產者消費者模型
(一)使用生產者消費者模型的原因
生產者消費者模式就是通過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通訊,而通過阻塞隊列來進行通訊
生產者生產完數據之后不用等待消費者處理,直接扔給阻塞隊列;消費者不找生產者要數據,而是直接從阻塞隊列里取
阻塞隊列就相當于一個緩沖區(qū),平衡了生產者和消費者的處理能力,這個阻塞隊列就是用來給生產者和消費者解耦的
(二)生產者消費者模型優(yōu)點
- 解耦
- 支持并發(fā)
- 支持忙閑不均
(三)基于BlockingQueue的生產者消費者模型
BlockingQueue
在多線程編程中阻塞隊列(Blocking Queue)是一種常用于實現生產者和消費者模型的數據結構。其與普通的隊列區(qū)別在于,當隊列為空時,從隊列獲取元素的操作將會被阻塞,直到隊列中被放入了元素;當隊列滿時,往隊列里存放元素的操作也會被阻塞,直到有元素被從隊列中取出
(四)對生產者消費者模型的總結
1. 有三種關系
- 生成者和生成者之間的關系:互斥
- 生產者和消費者之間的關系:互斥,同步
- 消費者和消費者之間的關系:互斥
2. 有兩者角色
生產者和消費者
3. 有一種交易場所
特定結構的內存空間
16. POSIX信號量
POSIX信號量和 SystemV信號量作用相同,都是用于同步操作,達到無沖突的訪問共享資源目的。 但POSIX可以用于線程間同步
17. 基于環(huán)形隊列的生產消費模型
(一)了解基于環(huán)形隊列的生產消費模型
環(huán)形隊列采用數組模擬,用模運算來模擬環(huán)狀特性
判斷隊列是否為空或者為滿,我們交給信號量去處理
注意:
- 信號量的本質是一個計數器,用來描述臨界資源的數目(這里不把臨界資源當成一個整體,而是很多份,多個線程并發(fā)訪問臨界資源中的不同份)
- 臨界資源是否就緒的判斷是在臨界區(qū)之外的(和前一個生產消費模型區(qū)分開來),在申請信號量的時候,就已經間接在做判斷了(這一步是預定操作,即預定成功,則整個臨界資源必有一份資源是屬于預定成功的線程的)
(二)基于環(huán)形隊列的生產消費模型的原理
?
18. 線程池
(一)了解線程池
線程池:
一種線程使用模式
線程過多會帶來調度開銷,進而影響緩存局部性和整體性能。而線程池維護著多個線程(提前開辟好的),等待著監(jiān)督管理者分配可并發(fā)執(zhí)行的任務。這避免了在處理短時間任務時創(chuàng)建與銷毀線程的代價。線程池不僅能夠保證內核的充分利用,還能防止過分調度
池化計數:
線程會用到池化技術(以空間換時間),大致操作(C++為例):現在C++類內創(chuàng)建線程(創(chuàng)建的線程當作類內成員變量),再利用原生線程去實現生產者消費者模型
注意:
- 在類內創(chuàng)建時,若線程執(zhí)行的函數也是類內函數,會出現錯誤(因為執(zhí)行的函數傳參只有一個void*,而類內函數會多一個this指針),所以該執(zhí)行函數必須是靜態(tài)的
- 若執(zhí)行函數是靜態(tài)的,同樣會出現一個問題,即靜態(tài)成員函數無法訪問非靜態(tài)成員變量(沒有this指針),所以執(zhí)行函數的參數void*必須是this指針,通過穿過來的this指針,訪問非靜態(tài)成員變量
(二)線程池的應用場景
- 需要大量的線程來完成任務,且完成任務的時間比較短。因為單個任務小,而任務數量巨大, 但對于長時間的任務,線程池的優(yōu)點就不明顯了
- 對性能要求苛刻的應用,比如要求服務器迅速響應客戶請求
- 接受突發(fā)性的大量請求,但不至于使服務器因此產生大量線程的應用。突發(fā)性大量客戶請求,在沒有線程池情況下,將產生大量線程,雖然理論上大部分操作系統(tǒng)線程數目最大值不是問題,短時間內產生大量線程可能使內存到達極限
(三)線程池示例
- 創(chuàng)建固定數量線程池,循環(huán)從任務隊列中獲取任務對象
- 獲取到任務對象后,執(zhí)行任務對象中的任務接口
19. 線程安全的單例模式
(一)什么是單例模式
單例模式是一種 "經典的, 常用的, 常考的" 設計模式(這個類的對象,只允許創(chuàng)建一個)
(二)什么是設計模式
針對一些經典的常見的場景, 給定了一些對應的解決方案, 這個就是 設計模式
(三)單例模式的特點
- 某些類, 只應該具有一個對象(實例), 就稱之為單例
- 在很多服務器開發(fā)場景中, 經常需要讓服務器加載很多的數據 (上百G) 到內存中. 此時往往要用一個單例的類來管理這些數據
(四)餓漢實現方式和懶漢實現方式
舉例類比(洗完的例子)
餓漢方式:
吃完飯, 立刻洗碗, 這種就是餓漢方式. 因為下一頓吃的時候可以立刻拿著碗就能吃飯
懶漢方式:
吃完飯, 先把碗放下, 然后下一頓飯用到這個碗了再洗碗, 就是懶漢方式
注意:
懶漢方式最核心的思想是 "延時加載". 從而能夠優(yōu)化服務器的啟動速度(在準備進程啟動時,需要把一些數據加載到內存,這一個過程的時間少)
餓漢方式 代碼實現
template <typename T> class Singleton { static T data; public: static T* GetInstance() { return &data; } };
data 是靜態(tài)類內成員,會在進程啟動之前,早就創(chuàng)建好,而不是等到啟動進程時,再去創(chuàng)建
?
懶漢模式 代碼實現
template <typename T> class Singleton { static T* inst; public: static T* GetInstance() { if (inst == nullptr) { inst = new T(); } return inst; } }; template <typename T> T* Singleton<T>:: inst = nullptr;
inst指針是要等待使用的時候,所指向的內容才會在堆上開辟新空間
(五)懶漢方式實現單例模式(線程安全版本)
代碼
static phread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; template <typename T> class Singleton { static T* inst; // 需要設置 volatile 關鍵字, 否則可能被編譯器優(yōu)化. public: static T* GetInstance() { if (inst == NULL) //這一次的判斷可以提高效率,使得沒有進入第二個判斷的線程,直接執(zhí)行非臨界資源的代碼 { lock.lock(); // 使用互斥鎖, 防止多個線程進入第二個判斷,保證多線程情況下也只調用一次 new if (inst == NULL) { inst = new T(); } lock.unlock(); }return inst; } };T* Singleton<T>:: inst = nullptr;
注意:(代碼不是完整的)
lock(),unlock()底層都是用原生線程庫封裝了
20. STL , 智能指針和線程安全
(一)常見問題
- STL中的容器是否是線程安全
不安全
原因:
STL 的設計初衷是將性能挖掘到極致, 而一旦涉及到加鎖保證線程安全, 會對性能造成巨大的影響.
而且對于不同的容器, 加鎖方式的不同, 性能可能也不同(例如hash表的鎖表和鎖桶).
因此 STL 默認不是線程安全. 如果需要在多線程環(huán)境下使用, 往往需要調用者自行保證線程安全.
- 智能指針是否是線程安全
- 對于 unique_ptr, 由于只是在當前代碼塊范圍內生效, 因此不涉及線程安全問題.
- 對于 shared_ptr, 多個對象需要共用一個引用計數變量, 所以會存在線程安全問題. 但是標準庫實現的時候考慮到了這個問題, 基于原子操作(CAS)的方式保證 shared_ptr 能夠高效, 原子的操作引用計數.
(二)其他常見的各種鎖
- 悲觀鎖:在每次取數據時,總是擔心數據會被其他線程修改,所以會在取數據前先加鎖(讀鎖,寫鎖,行鎖等),當其他線程想要訪問數據時,被阻塞掛起
- 樂觀鎖:每次取數據時候,總是樂觀的認為數據不會被其他線程修改,因此不上鎖。但是在更新數據前,會判斷其他數據在更新前有沒有對數據進行修改。主要采用兩種方式:版本號機制和CAS操作
- CAS操作:當需要更新數據時,判斷當前內存值和之前取得的值是否相等。如果相等則用新值更新。若不等則失敗,失敗則重試,一般是一個自旋的過程,即不斷重試。
- 自旋鎖(是否采用自旋鎖,取決于臨界資源訪問的時間的長短,時間短,可以采用自旋鎖,即線程不掛起,一直申請所資源)
21. 讀者寫者問題
讀寫鎖
在編寫多線程的時候,有一種情況是十分常見的:有些公共數據修改的機會比較少,相比較改寫,它們讀的機會反而高的多。
讀寫鎖可以專門處理這種多讀少寫的情況
注意:寫和寫競爭,寫和讀競爭且同步,讀和讀共享資源,讀鎖優(yōu)先級高
相關函數
初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t
*restrict attr);
銷毀
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加鎖和解鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); //寫與寫競爭
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //讀與寫競爭
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
代碼(部分)
void * reader(void * arg) //讀端 { char *id = (char *)arg; while (1) { pthread_rwlock_rdlock(&rwlock); if () //判斷 { pthread_rwlock_unlock(&rwlock); break; } pthread_rwlock_unlock(&rwlock); } return nullptr; }void * writer(void * arg) //寫端 { char *id = (char *)arg; while (1) { pthread_rwlock_wrlock(&rwlock); if () //判斷條件 { pthread_rwlock_unlock(&rwlock); break; }} return nullptr; }
中間一些過程省略了
底層實現(偽代碼)
int reader_count = 0; mutex_t rlock,wlock; 讀端: lock(&rlock) reader_count++; if(reader_count == 1) //第一個競爭到所資源的讀端 {lock(&wlock); //寫段不能訪問 } unlock(&rlock)lock(&rlock); reader_count--; if(reader_count == 0)//一開始一批讀端的最后一個 {unlock(&wlock); //此時讀端可以競爭 } unlock(&rlock);寫端: lock(&wlock); // ... 寫入 unlock(&wlock);