一、同步和死鎖
在前面學習多線程和網絡編程時,都對線程中數據的同步和數據結構多線程訪問的安全問題進行了分析和說明。其實,多線程編程之所以難,難點之一就在這里,數據同步意味著效率和安全的平衡,而這里的安全有一個重要的關節就在于死鎖。
所以如果能很好的掌握線程間的同步以及防止出現死鎖這些問題后,基本對多線程的理解和控制就算是入了門。
二、死鎖場景
死鎖其實在大多數的開發者實際的開發中遇到的并不多。即使遇到也是在應用層面上用到,很少有主動寫出死鎖的。但這不代表死鎖少見,一般來說,常見的死鎖出現的場景有以下幾方面:
1、數據庫中的事務處理
比如多個事務都需要同時處理多張表,這里假在鎖定一些表后,有可能導致死鎖
2、多線程編程
這種情況下剛剛也提到了,較少有開發者會寫出這種死鎖的代碼,主要原因是,一般的開發場景也不會有多個資源在線程間同時訪問,即使有,很多開發者也會分開處理,等待通知后再進行
3、在辦公中訪問一些打印機等專享資源
這種情況其實比較常見,有時候兒打印機(包括文件等)啥的與OS或網絡的交互會犯傻,這時個兒往往重啟一下就好了
4、分布式應用中的同步請求
這種情況在分布式應用中比較常見,比如P2P中的互相通信如果操作不當,就有可能產生死鎖
5、大規模計算中的資源分配
比如在一些大規模計算中使用計算圖,就有可能在圖的處理中資源分配出現死鎖問題
三、避免死鎖
要想避免死鎖就必須了妥死鎖的條件,學習過操作系統相關知識的開發者都知道,死鎖的四個條件:
1、資源互斥
這個比較容易理解,資源必須是獨占的,即一個資源只能被一個線程占用。這就和食堂排隊打湯的一樣,大勺只有一個,只有一個人打好放勺子才可能另外一個人去使用
2、互相持有
也叫占有和等待,即一個線程完成一項工作需要兩個以上的資源,已經占有了一個,申請另外一個時,發現另外一個資源被其它線程占有,它會繼續申請而不釋放自己持有的資源。這個更好理解,就比如集一些類似火花、郵票或卡之類的東西(假設每個都是唯一的),如果三個人分別持有一套的三部分,大家都會尋找其它的而不會把自己的部分放棄
3、不可剝奪
即任何一個線程持有的資源無法被其它線程強制獲取。這個好理解,正常情況下,公民的財產不會被其它公民強行取得
4、循環等待
這個理解也不難,就是多個線程形成了一個環狀的資源持有,即A線程持有B線程需要的資源,B線程持有C線程的需要的資源…N線程持有A線程需要的資源。這就是書本上講的哲學家進餐的問題。
既然知道了死鎖造成的原因,那么解決死鎖就必須從上面的原因中找方法,只要打破任何一個條件,死鎖也就不存在了,即:
1、打破資源互斥,即將資源設計為允許多個線程訪問
2、打破互相持有,即所有的線程申請資源時一次性申請完成,要么成功,要么失敗,也或者可以在申請前把自己持有的資源釋放
3、打破不可剝奪,即允許在指定條件下獲取某些線程占有的資源
4、打破循環等待,即對資源進行有序控制不允許隨機申請,這樣就會斷開循環的鏈條
四、死鎖的解決方案
知道了避免死鎖的方法,就可以探討解決死鎖的編程方案了。一般來說,解決死鎖的方案有以下幾種:
1、無鎖編程
這種最容易理解了,打不過就躲過嘛。無鎖編程在前面分析說明了很多,此處不再展開
2、使用超時鎖
這個可以打破占有和等待。在C++11中提供了一些新的鎖如 std::timed_mutex 和 std::recursive_timed_mutex,看下面的例子:
#include <chrono>
#include <iostream>
#include <mutex>
#include <sstream>
#include <thread>
#include <vector>using namespace std::chrono_literals;std::mutex cout_mutex; // 控制到 std::cout 的訪問
std::timed_mutex mutex;void job(int id)
{std::ostringstream stream;for (int i = 0; i < 3; ++i){if (mutex.try_lock_for(100ms)){stream << "成功 ";std::this_thread::sleep_for(100ms);mutex.unlock();}elsestream << "失敗 ";std::this_thread::sleep_for(100ms);}std::lock_guard<std::mutex> lock{cout_mutex};std::cout << "[" << id << "] " << stream.str() << "\n";
}int main()
{std::vector<std::thread> threads;for (int i = 0; i < 4; ++i)threads.emplace_back(job, i);for (auto& i: threads)i.join();
}
3、可以將鎖排序
用來打破循環等待等條件,這種應用比較簡單,看下面的例子:
#include <iostream>
#include <mutex>
#include <thread>
#include <unistd.h>std::mutex m1;
std::mutex m2;void taskFunc(bool order) {if (order) {// reversestd::lock_guard<std::mutex> lock2(m2);sleep(2);std::lock_guard<std::mutex> lock1(m1);std::cout << "lock risky!\n";} else {// orderstd::lock_guard<std::mutex> lock1(m1);sleep(2);std::lock_guard<std::mutex> lock2(m2);std::cout << "lock safe!\n";}
}int main() {std::thread t1([]() { taskFunc(true); });std::thread t2([]() { taskFunc(false); });t1.join();t2.join();return 0;
}
上面的代碼會產生死鎖,可以強制進行結束。
4、一次性獲取鎖
在C++17中提供了std::scoped_lock,相關的代碼可以查看前面的文章“跟我學C++中級篇——std::scoped_lock”
5、控制資源訪問
這種就方法就比較多了,比如在Windows平臺可以使用事件控制而在Linux平臺上使用條件變量進行,即沒有收到通知的一方不能去尋求資源的控制。典型的就是在Nginx的網絡資源獲取中就采用了這種控制的手段
如果在編程時沒有注意,在實際的應用中出現了意外,可以使用一些工具來檢查是否存在死鎖:
1、使用GDB
在編譯后,使用gdb:
gdb ./lockOrder
(gdb) r
Starting program: /home/fpc/qt65_project/lockOrder/lockOrder
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff4eff640 (LWP 843994)]
[New Thread 0x7ffff46fe640 (LWP 843995)]
[New Thread 0x7ffff3efd640 (LWP 843996)]
//此處如果不退出可以ctrl+C強制退出來
(gdb) info threadId Target Id Frame
* 1 Thread 0x7ffff7317340 (LWP 843991) "lockOrder" __futex_abstimed_wait_common64 (private=128, cancel=true, abstime=0x0, op=265, expected=843995, futex_word=0x7ffff46fe910) at ./nptl/futex-internal.c:572 Thread 0x7ffff4eff640 (LWP 843994) "lockOrder" 0x00007ffff6ce57f8 in __GI___clock_nanosleep (clock_id=clock_id@entry=0, flags=flags@entry=0, req=req@entry=0x7ffff4ebf150, rem=rem@entry=0x0) at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:783 Thread 0x7ffff46fe640 (LWP 843995) "lockOrder" futex_wait (private=0, expected=2, futex_word=0x55555555c160 <m1>) at ../sysdeps/nptl/futex-internal.h:1464 Thread 0x7ffff3efd640 (LWP 843996) "lockOrder" futex_wait (private=0, expected=2, futex_word=0x55555555c1a0 <m2>) at ../sysdeps/nptl/futex-internal.h:146
(gdb)
這樣就可以看到具體的死鎖的情況了
2、使用valgrind并啟用Helgrind工具
執行下列命令可得到相關日志:
valgrind --tool=helgrind --log-file=log.txt ./lockOrder
日志顯示(未貼全):
Thread #3: Exiting thread still holds 1 lock
==849869== at 0x4B572C0: futex_wait (futex-internal.h:146)
==849869== by 0x4B572C0: __lll_lock_wait (lowlevellock.c:49)
==849869== by 0x4B5E001: lll_mutex_lock_optimized (pthread_mutex_lock.c:48)
==849869== by 0x4B5E001: pthread_mutex_lock@@GLIBC_2.2.5 (pthread_mutex_lock.c:93)
==849869== by 0x485051D: mutex_lock_WRK (hg_intercepts.c:935)
==849869== by 0x4854CEE: pthread_mutex_lock (hg_intercepts.c:958)
==849869== by 0x109DE1: __gthread_mutex_lock(pthread_mutex_t*) (in /home/fpc/qt65_project/lockOrder/build/lockOrder)
==849869== by 0x109E65: std::mutex::lock() (in /home/fpc/qt65_project/lockOrder/build/lockOrder)
==849869== by 0x10A09D: std::lock_guard<std::mutex>::lock_guard(std::mutex&) (in /home/fpc/qt65_project/lockOrder/build/lockOrder)
==849869== by 0x109456: taskFunc(bool) (in /home/fpc/qt65_project/lockOrder/build/lockOrder)
==849869== by 0x109539: main::{lambda()#2}::operator()() const (in /home/fpc/qt65_project/lockOrder/build/lockOrder)
...
3、ThreadSanitizer (TSan)
它是集成于GCC和Clang的檢測工具,使用方法如下:
g++ -fsanitize=thread -g -o lockorder main.cpp
./lockorder
4、Lttng
這個沒有實驗成功,暫時沒找到原因。不過據說這個挺好用
5、其它
在不同的平臺上可能有不同的工具,比如在Windows平臺上的有名的工具Windbg和VS(ASan)等,安卓平臺上也有類似的工具;另外像前面介紹的Perf Tools工具,也可以間接的輔助進行線程死鎖的定位
五、總結
死鎖雖然在面試時反復被問到,但在實踐中真正寫出來或者遇到的并沒有想象的那么多。其實最主要的原因就是大多數的程序員都不會有這種開發的應用場景。但恰恰因為遇到的少,在實際中真正出現時,卻不知道從何下手。
還是老規矩,把基礎掌握好,會靈活的使用工具。只要發現定位了死鎖的問題,就可以根據產生死鎖的原因有針對的進行解決即可。沒有過不了的火焰山。