什么死鎖怎么排查死鎖?怎么避免死鎖?

      網友投稿 914 2022-05-30

      突然發現我的圖解系統缺了「死鎖」的內容,這就來補下。

      在面試過程中,死鎖也是高頻的考點,因為如果線上環境真多發生了死鎖,那真的出大事了。

      這次,我們就來系統地聊聊死鎖的問題。

      死鎖的概念;

      模擬死鎖問題的產生;

      利用工具排查死鎖問題;

      避免死鎖問題的發生;

      死鎖的概念

      在多線程編程中,我們為了防止多線程競爭共享資源而導致數據錯亂,都會在操作共享資源之前加上互斥鎖,只有成功獲得到鎖的線程,才能操作共享資源,獲取不到鎖的線程就只能等待,直到鎖被釋放。

      那么,當兩個線程為了保護兩個不同的共享資源而使用了兩個互斥鎖,那么這兩個互斥鎖應用不當的時候,可能會造成兩個線程都在等待對方釋放鎖,在沒有外力的作用下,這些線程會一直相互等待,就沒辦法繼續運行,這種情況就是發生了死鎖。

      舉個例子,小林拿了小美房間的鑰匙,而小林在自己的房間里,小美拿了小林房間的鑰匙,而小美也在自己的房間里。如果小林要從自己的房間里出去,必須拿到小美手中的鑰匙,但是小美要出去,又必須拿到小林手中的鑰匙,這就形成了死鎖。

      死鎖只有同時滿足以下四個條件才會發生:

      互斥條件;

      持有并等待條件;

      不可剝奪條件;

      環路等待條件;

      互斥條件是指多個線程不能同時使用同一個資源。

      比如下圖,如果線程 A 已經持有的資源,不能再同時被線程 B 持有,如果線程 B 請求獲取線程 A 已經占用的資源,那線程 B 只能等待,直到線程 A 釋放了資源。

      持有并等待條件是指,當線程 A 已經持有了資源 1,又想申請資源 2,而資源 2 已經被線程 C 持有了,所以線程 A 就會處于等待狀態,但是線程 A 在等待資源 2 的同時并不會釋放自己已經持有的資源 1。

      不可剝奪條件是指,當線程已經持有了資源 ,在自己使用完之前不能被其他線程獲取,線程 B 如果也想使用此資源,則只能在線程 A 使用完并釋放后才能獲取。

      環路等待條件指都是,在死鎖發生的時候,兩個線程獲取資源的順序構成了環形鏈。

      比如,線程 A 已經持有資源 2,而想請求資源 1, 線程 B 已經獲取了資源 1,而想請求資源 2,這就形成資源請求等待的環形圖。

      模擬死鎖問題的產生

      Talk is cheap. Show me the code.

      下面,我們用代碼來模擬死鎖問題的產生。

      首先,我們先創建 2 個線程,分別為線程 A 和 線程 B,然后有兩個互斥鎖,分別是 mutex_A 和 mutex_B,代碼如下:

      pthread_mutex_t mutex_A = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex_B = PTHREAD_MUTEX_INITIALIZER; int main() { pthread_t tidA, tidB; //創建兩個線程 pthread_create(&tidA, NULL, threadA_proc, NULL); pthread_create(&tidB, NULL, threadB_proc, NULL); pthread_join(tidA, NULL); pthread_join(tidB, NULL); printf("exit\n"); return 0; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      接下來,我們看下線程 A 函數做了什么。

      //線程函數 A void *threadA_proc(void *data) { printf("thread A waiting get ResourceA \n"); pthread_mutex_lock(&mutex_A); printf("thread A got ResourceA \n"); sleep(1); printf("thread A waiting get ResourceB \n"); pthread_mutex_lock(&mutex_B); printf("thread A got ResourceB \n"); pthread_mutex_unlock(&mutex_B); pthread_mutex_unlock(&mutex_A); return (void *)0; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      可以看到,線程 A 函數的過程:

      先獲取互斥鎖 A,然后睡眠 1 秒;

      再獲取互斥鎖 B,然后釋放互斥鎖 B;

      最后釋放互斥鎖 A;

      //線程函數 B void *threadB_proc(void *data) { printf("thread B waiting get ResourceB \n"); pthread_mutex_lock(&mutex_B); printf("thread B got ResourceB \n"); sleep(1); printf("thread B waiting get ResourceA \n"); pthread_mutex_lock(&mutex_A); printf("thread B got ResourceA \n"); pthread_mutex_unlock(&mutex_A); pthread_mutex_unlock(&mutex_B); return (void *)0; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      可以看到,線程 B 函數的過程:

      先獲取互斥鎖 B,然后睡眠 1 秒;

      再獲取互斥鎖 A,然后釋放互斥鎖 A;

      最后釋放互斥鎖 B;

      然后,我們運行這個程序,運行結果如下:

      thread B waiting get ResourceB thread B got ResourceB thread A waiting get ResourceA thread A got ResourceA thread B waiting get ResourceA thread A waiting get ResourceB // 阻塞中。。。

      1

      2

      3

      4

      5

      6

      7

      可以看到線程 B 在等待互斥鎖 A 的釋放,線程 A 在等待互斥鎖 B 的釋放,雙方都在等待對方資源的釋放,很明顯,產生了死鎖問題。

      利用工具排查死鎖問題

      如果你想排查你的 Java 程序是否死鎖,則可以使用 jstack 工具,它是 jdk 自帶的線程堆棧分析工具。

      由于小林的死鎖代碼例子是 C 寫的,在 Linux 下,我們可以使用 pstack + gdb 工具來定位死鎖問題。

      pstack 命令可以顯示每個線程的棧跟蹤信息(函數調用過程),它的使用方式也很簡單,只需要 pstack 就可以了。

      那么,在定位死鎖問題時,我們可以多次執行 pstack 命令查看線程的函數調用過程,多次對比結果,確認哪幾個線程一直沒有變化,且是因為在等待鎖,那么大概率是由于死鎖問題導致的。

      我用 pstack 輸出了我前面模擬死鎖問題的進程的所有線程的情況,我多次執行命令后,其結果都一樣,如下:

      $ pstack 87746 Thread 3 (Thread 0x7f60a610a700 (LWP 87747)): #0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0 #1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0 #2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0 #3 0x0000000000400725 in threadA_proc () #4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0 #5 0x00000037206f4bfd in clone () from /lib64/libc.so.6 Thread 2 (Thread 0x7f60a5709700 (LWP 87748)): #0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0 #1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0 #2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0 #3 0x0000000000400792 in threadB_proc () #4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0 #5 0x00000037206f4bfd in clone () from /lib64/libc.so.6 Thread 1 (Thread 0x7f60a610c700 (LWP 87746)): #0 0x0000003720e080e5 in pthread_join () from /lib64/libpthread.so.0 #1 0x0000000000400806 in main () .... $ pstack 87746 Thread 3 (Thread 0x7f60a610a700 (LWP 87747)): #0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0 #1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0 #2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0 #3 0x0000000000400725 in threadA_proc () #4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0 #5 0x00000037206f4bfd in clone () from /lib64/libc.so.6 Thread 2 (Thread 0x7f60a5709700 (LWP 87748)): #0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0 #1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0 #2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0 #3 0x0000000000400792 in threadB_proc () #4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0 #5 0x00000037206f4bfd in clone () from /lib64/libc.so.6 Thread 1 (Thread 0x7f60a610c700 (LWP 87746)): #0 0x0000003720e080e5 in pthread_join () from /lib64/libpthread.so.0 #1 0x0000000000400806 in main ()

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      什么是死鎖?怎么排查死鎖?怎么避免死鎖?

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

      37

      38

      39

      可以看到,Thread 2 和 Thread 3 一直阻塞獲取鎖(pthread_mutex_lock)的過程,而且 pstack 多次輸出信息都沒有變化,那么可能大概率發生了死鎖。

      但是,還不能夠確認這兩個線程是在互相等待對方的鎖的釋放,因為我們看不到它們是等在哪個鎖對象,于是我們可以使用 gdb 工具進一步確認。

      整個 gdb 調試過程,如下:

      // gdb 命令 $ gdb -p 87746 // 打印所有的線程信息 (gdb) info thread 3 Thread 0x7f60a610a700 (LWP 87747) 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0 2 Thread 0x7f60a5709700 (LWP 87748) 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0 * 1 Thread 0x7f60a610c700 (LWP 87746) 0x0000003720e080e5 in pthread_join () from /lib64/libpthread.so.0 //最左邊的 * 表示 gdb 鎖定的線程,切換到第二個線程去查看 // 切換到第2個線程 (gdb) thread 2 [Switching to thread 2 (Thread 0x7f60a5709700 (LWP 87748))]#0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0 // bt 可以打印函數堆棧,卻無法看到函數參數,跟 pstack 命令一樣 (gdb) bt #0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0 #1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0 #2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0 #3 0x0000000000400792 in threadB_proc (data=0x0) at dead_lock.c:25 #4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0 #5 0x00000037206f4bfd in clone () from /lib64/libc.so.6 // 打印第三幀信息,每次函數調用都會有壓棧的過程,而 frame 則記錄棧中的幀信息 (gdb) frame 3 #3 0x0000000000400792 in threadB_proc (data=0x0) at dead_lock.c:25 27 printf("thread B waiting get ResourceA \n"); 28 pthread_mutex_lock(&mutex_A); // 打印mutex_A的值 , __owner表示gdb中標示線程的值,即LWP (gdb) p mutex_A $1 = {__data = {__lock = 2, __count = 0, __owner = 87747, __nusers = 1, __kind = 0, __spins = 0, __list = {__prev = 0x0, __next = 0x0}}, __size = "\002\000\000\000\000\000\000\000\303V\001\000\001", '\000' , __align = 2} // 打印mutex_B的值 , __owner表示gdb中標示線程的值,即LWP (gdb) p mutex_B $2 = {__data = {__lock = 2, __count = 0, __owner = 87748, __nusers = 1, __kind = 0, __spins = 0, __list = {__prev = 0x0, __next = 0x0}}, __size = "\002\000\000\000\000\000\000\000\304V\001\000\001", '\000' , __align = 2}

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

      37

      38

      我來解釋下,上面的調試過程:

      通過 info thread 打印了所有的線程信息,可以看到有 3 個線程,一個是主線程(LWP 87746),另外兩個都是我們自己創建的線程(LWP 87747 和 87748);

      通過 thread 2,將切換到第 2 個線程(LWP 87748);

      通過 bt,打印線程的調用棧信息,可以看到有 threadB_proc 函數,說明這個是線程 B 函數,也就說 LWP 87748 是線程 B;

      通過 frame 3,打印調用棧中的第三個幀的信息,可以看到線程 B 函數,在獲取互斥鎖 A 的時候阻塞了;

      通過 p mutex_A,打印互斥鎖 A 對象信息,可以看到它被 LWP 為 87747(線程 A) 的線程持有著;

      通過 p mutex_B,打印互斥鎖 A 對象信息,可以看到他被 LWP 為 87748 (線程 B) 的線程持有著;

      因為線程 B 在等待線程 A 所持有的 mutex_A, 而同時線程 A 又在等待線程 B 所擁有的mutex_B, 所以可以斷定該程序發生了死鎖。

      避免死鎖問題的發生

      前面我們提到,產生死鎖的四個必要條件是:互斥條件、持有并等待條件、不可剝奪條件、環路等待條件。

      那么避免死鎖問題就只需要破環其中一個條件就可以,最常見的并且可行的就是使用資源有序分配法,來破環環路等待條件。

      那什么是資源有序分配法呢?

      線程 A 和 線程 B 獲取資源的順序要一樣,當線程 A 是先嘗試獲取資源 A,然后嘗試獲取資源 B 的時候,線程 B 同樣也是先嘗試獲取資源 A,然后嘗試獲取資源 B。也就是說,線程 A 和 線程 B 總是以相同的順序申請自己想要的資源。

      我們使用資源有序分配法的方式來修改前面發生死鎖的代碼,我們可以不改動線程 A 的代碼。

      我們先要清楚線程 A 獲取資源的順序,它是先獲取互斥鎖 A,然后獲取互斥鎖 B。

      所以我們只需將線程 B 改成以相同順序的獲取資源,就可以打破死鎖了。

      線程 B 函數改進后的代碼如下:

      //線程 B 函數,同線程 A 一樣,先獲取互斥鎖 A,然后獲取互斥鎖 B void *threadB_proc(void *data) { printf("thread B waiting get ResourceA \n"); pthread_mutex_lock(&mutex_A); printf("thread B got ResourceA \n"); sleep(1); printf("thread B waiting get ResourceB \n"); pthread_mutex_lock(&mutex_B); printf("thread B got ResourceB \n"); pthread_mutex_unlock(&mutex_B); pthread_mutex_unlock(&mutex_A); return (void *)0; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      執行結果如下,可以看,沒有發生死鎖。

      thread B waiting get ResourceA thread B got ResourceA thread A waiting get ResourceA thread B waiting get ResourceB thread B got ResourceB thread A got ResourceA thread A waiting get ResourceB thread A got ResourceB exit

      1

      2

      3

      4

      5

      6

      7

      8

      9

      總結

      簡單來說,死鎖問題的產生是由兩個或者以上線程并行執行的時候,爭奪資源而互相等待造成的。

      死鎖只有同時滿足互斥、持有并等待、不可剝奪、環路等待這四個條件的時候才會發生。

      所以要避免死鎖問題,就是要破壞其中一個條件即可,最常用的方法就是使用資源有序分配法來破壞環路等待條件。

      任務調度

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:二十五個軟件測試經典面試題,你招架得住嗎?
      下一篇:鴻蒙系統概述
      相關文章
      亚洲成AV人网址| 亚洲人成激情在线播放| 亚洲高清一区二区三区电影| 亚洲人成网站日本片| 亚洲国产精品综合福利专区| 亚洲黄色免费观看| 91亚洲精品第一综合不卡播放| 99久久精品国产亚洲| 亚洲欧洲在线播放| 久久精品亚洲AV久久久无码| 亚洲依依成人精品| 亚洲综合欧美色五月俺也去| 亚洲色大网站WWW永久网站| 亚洲精品无码日韩国产不卡av| 亚洲人成人伊人成综合网无码| 亚洲精品无码mⅴ在线观看| 亚洲成a∧人片在线观看无码 | 亚洲AV永久无码精品成人| 亚洲高清国产拍精品26U| 亚洲人成电影亚洲人成9999网 | 精品亚洲国产成人av| 国产精品亚洲精品日韩电影| 亚洲国产精品成人网址天堂| 在线亚洲精品自拍| 亚洲国产精品一区第二页| 亚洲视频在线一区| 亚洲国产成人精品无码区在线秒播| 亚洲videos| 国产成人精品日本亚洲语音| 亚洲国产专区一区| 国精无码欧精品亚洲一区| 久久丫精品国产亚洲av| 亚洲日本在线播放| 亚洲另类无码一区二区三区| 亚洲AV成人潮喷综合网| 国产亚洲一区二区手机在线观看 | 午夜亚洲国产理论片二级港台二级 | 亚洲精品第一综合99久久| 色五月五月丁香亚洲综合网| 亚洲七七久久精品中文国产| 亚洲日韩乱码中文无码蜜桃臀网站|