面试三连:什么是死锁?怎么排查死锁?怎么避免死锁?(下)

发表于:2021-5-26 09:47

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:不秃顶的Java程序员    来源:掘金

#
死锁
  02 模拟死锁问题的产生
  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;
  }
  接下来,我们看下线程 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;
  }
  可以看到,线程 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;
  }
  可以看到,线程 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 
  // 阻塞中。。。
  可以看到线程 B 在等待互斥锁 A 的释放,线程 A 在等待互斥锁 B 的释放,双方都在等待对方资源的释放,很明显,产生了死锁问题。
  03 利用工具排查死锁问题
  如果你想排查你的 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 ()
  可以看到,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' <repeats 26 times>, __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' <repeats 26 times>, __align = 2}
  我来解释下,上面的调试过程:
  通过 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, 所以可以断定该程序发生了死锁。
  04 避免死锁问题的发生
  前面我们提到,产生死锁的四个必要条件是:互斥条件、持有并等待条件、不可剥夺条件、环路等待条件。
  那么避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。
  那什么是资源有序分配法呢?
  线程 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;
  }
  执行结果如下,可以看,没有发生死锁。
  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
  总结
  简单来说,死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。
  死锁只有同时满足互斥、持有并等待、不可剥夺、环路等待这四个条件的时候才会发生。
  所以要避免死锁问题,就是要破坏其中一个条件即可,最常用的方法就是使用资源有序分配法来破坏环路等待条件。

      本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号