muduo学习笔记 - 第2章 线程同步精要
第2章 線程同步精要
2.1 互斥器 (mutex)
互斥器保護了臨界區,任何時刻最多只能有一個線程在mutex劃出的臨界區內活動
推薦使用原則:
- 用RAII手法封裝mutex的創建、銷毀、加鎖、解鎖四個操作
- 只是用非遞歸的mutex (不可重入的mutex)
- 不手動調用lock()和unlock()函數,交給棧上的Guard對象的構造和析構負責
- 使用Guard對象時考慮調用棧上持有的鎖,防止加鎖順序不同導致死鎖
非遞歸的mutex
mutex分為遞歸和非遞歸,也稱為可重入和非可重入。區別在于:同一個線程可以重復對recursive mutex加鎖,不能重復對non-recursive mutex加鎖。
在同一線程中對non-recursive mutex重復加鎖會立刻導致死鎖,可以幫助在編碼階段發現問題。
recursive mutex可能會隱藏一些問題,當你以為拿到一個鎖就能修改對象,沒想到外層代碼已經拿到了鎖,增在修改或讀取同一個對象
MutexLock mutex; std::vector<Foo> foos;void post(const Foo& f) { // 加鎖修改對象MutexLockGuard lock(mutex);foos.push_back(f); }void traverse() { // 加鎖訪問對象MutexLockGuard lock(mutex); for (std::vector<Foo>::const_iterator it = foos.begin();it != foos.end(); ++it) {it->doit();} }如果Foo::doit()間接調用post,Mutex為非遞歸就會發生死鎖,Mutex為遞歸可能會導致vector的迭代器失效,程序偶爾crash
解決上面問題有兩種做法:
- 把修改推遲,記錄循環中試圖添加修改的元素,等到循環結束再調用post
- 如果一個函數可能在加鎖的情況被調用,也可能在為假鎖的情況被調用,可以把函數拆成兩部分
- 跟原函數同名,函數加鎖,調用第二個函數
- 函數名加后綴WithLockHold,不加鎖,把原來的函數體搬過來
如上也會造成兩個問題:
對于(1)可以通過調用棧進行排錯,對于(2)可以在調用的時候判斷鎖是否時調用線程加的 (isLockedByThisThread)
2.2 條件變量
-
條件變量和mutex一起使用, 布爾表達式受mutex保護
-
將布爾條件判斷和wait放到while循環中
muduo::MutexLock mutex; muduo::Condition cond(mutex); std::deque<int> queue;int dequeue() { // 出隊MutexLockGuard lock(mutex);while (queue.empty()) { // 用循環先判斷在waitcond.wait(); // 原子操作,unlock mutex進入等待,不與enqueue競爭鎖// wait()執行完重新加鎖} }void enqueue(int x) {MutexLockGuard lock(mutex);queue.push_back(x);cond.notify(); }上面代碼必須用while循環等待條件變量, 不能用if語句, 可能存在spurious wakeup
-
broadcast通常用于表明狀態變化, signal通常用于表示資源可用
-
條件變量是底層的同步原語,通常用來實現高層的同步措施
-
BlockingQueue
-
DountDownLatch (倒計時)
- 主線程發起多個子線程,等待子線程各自完成一定任務后, 主線程繼續執行, 通常用于主線程等待多個子線程完成初始化
- 主線程發起多個子線程, 子線程等待主線程, 主線程完成一定任務后通知所有的子線程開始執行,通常用于多個子線程等待主線程發出“起跑”命令
-
2.3 不要用讀寫鎖和信號量
對寫鎖
-
典型錯誤,在持有read lock時候修改共享數據。不小心在read lock保護的函數中調用了會修改狀態的函數。
-
read lock加鎖的開銷不比mutex lock小,每次要更新reader的個數
-
reader lock的可重入可能造成死鎖
-
線程1加讀鎖,進行讀操作
-
線程2加寫鎖, 等待線程1,阻塞后面的讀操作
-
線程1內部間接調用對操作,因為reader lock的可重入,線程1的讀操作阻塞
-
發生死鎖
信號量
作者不建議用互斥量
對于哲學家就餐問題,在教科書的解決方案是平權,每個哲學家有自己的線程,自己去拿筷子。作者認為集權的方式,用一個線程專門負責餐具的分配,讓其他哲學家拿著號等在食堂門口(condition variable),這樣不損失多少效率同時簡化程序
2.4 小結
- 線程同步盡量用高層同步設施(線程池、隊列、倒計時)
- 讓一個正確的程序變快,遠比“讓一個快的程序變正確”容易的多
- 真正影響性能的不是鎖,而是鎖爭用(lock contention)
- sleep不是同步原語,盡量少使用,可以采用喚醒、輪詢、timer的方式
- 使用shared_ptr,讀操作增加引用計數,寫的時候用shared_ptr::unique()判斷是否有其他用戶在讀。如果沒有用戶讀直接修改,如果有可以拷貝一份,在副本上修改。使用shared_ptr::swap()更新指針
總結
以上是生活随笔為你收集整理的muduo学习笔记 - 第2章 线程同步精要的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: muduo学习笔记 - 第1章 C++多
- 下一篇: muduo学习笔记 - 第3章 多线程服