目录

1.代码演示死锁

2.死锁的定义

3.形象理解死锁

4.产生死锁的四个必要条件

解释"头尾相接的循环等待资源"

注意是必要条件!

5.解决死锁的方法

破坏互斥条件

破坏请求与保持条件

破坏不剥夺条件

破坏循环等待条件

避免死锁的方法

6.避免死锁算法

死锁检测算法

银行家算法

4.结论

5.补充: 线程不能自己等自己,否则死锁

man手册说明

glibc-2.43源码说明


1.代码演示死锁

OS74.【Linux】线程互斥(2) 加锁、解锁、“原子的“含义文章的代码为例子,稍作修改:

在get_tickets中调用两次pthread_mutex_lock(lock)函数

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <string>
#include <iostream>
#define NUM 5
int tickets=1000;//全局变量
class thread_data
{
public:
    thread_data(std::string* thread_name,pthread_mutex_t* lock)
    :_thread_name(thread_name)
    ,_lock(lock)
    {}
    std::string* _thread_name;
    pthread_mutex_t* _lock;
};

void* get_tickets(void* args)
{
    thread_data* tmp=static_cast<thread_data*>(args);
    pthread_mutex_t* lock=tmp->_lock;
    std::string* thread_name=tmp->_thread_name;
    
    for (;;)
    {
        pthread_mutex_lock(lock);
        pthread_mutex_lock(lock);
        if (tickets>0)
        {
            usleep(1000);
            tickets--;
            std::cout<<*thread_name<<" : "<<"完成抢票"<<"剩余"<<tickets<<"张票"<<std::endl;
        }
        else
        {
            pthread_mutex_unlock(lock);
            break;
        }
        pthread_mutex_unlock(lock);
        usleep(10);
    }
    
    //模拟抢票需要花时间
    return nullptr;
}

int main()
{
    pthread_t tid_arr[NUM];
    pthread_mutex_t lock;
    pthread_mutex_init(&lock,nullptr);
    for (int i=0;i<NUM;i++)
    {
        std::string* thread_name=new std::string("thread"+std::to_string(i));
        thread_data* args=new thread_data(thread_name,&lock);
        pthread_create(&tid_arr[i],nullptr,get_tickets,args);
    }
    for (int i=0;i<NUM;i++)
    {
        pthread_join(tid_arr[i],nullptr);
    }
    pthread_mutex_destroy(&lock);
    return 0;
}

运行结果: 卡住了

原因: 只有一把锁,但是申请两次,那么所有线程都卡住了

2.死锁的定义

例如两个线程都持有锁,但它们都想申请对方的锁 → 我等你,你等我,那么两个线程都处于永久等待的状态,即死锁

当然一个进程只有一个线程也能出现死锁,比如只有一个锁,但是该线程申请多次锁,导致死锁

死锁(deadlock)是指在一组进程中的各个线程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态

3.形象理解死锁

根据死锁的定义,可以这样形象理解死锁:

面试官:什么是死锁?
面试者:你给我 offer 我就告诉你
面试官:你说了我就给你 offer
周而复始

4.产生死锁的四个必要条件

这四个必要条件在各个大厂的面试题都考过:

2026字节后端(感谢快来一起摸鱼牛友分享面试题!)

2026字节测试开发实习一面(感谢不上岸不改名58牛友分享面试题!)

"必要条件"的含义: 只要产生死锁,一定能推出以下四个条件 

1.互斥条件(前提): 一个资源每次只能被一个执行流使用
2.请求与保持条件(原则): 一个执行流因请求资源而阻塞时,对已获得的资源保持不放
3.不剥夺条件(原则): 一个执行流已获得的资源,在未使用完之前,不能被强行剥夺
4.循环等待条件(★★★): 若干执行流之间形成一种头尾相接的循环等待资源的关系

解释"头尾相接的循环等待资源"

A等待B的资源,B等待C的资源,C等待D的资源,......,N等待A的资源,也就形成了环路(头尾相接)

注意是必要条件!

"产生死锁 → 存在环路"但是"产生死锁 存在环路"

Bilibili三面面经:

可以出现有环但是无死锁的情况

  • 当每种资源类型只有一个实例时,死锁一定发生

  • 如果资源类型有多个实例,系统也可能通过资源的动态分配来避免死锁

参考资料: https://zhuanlan.zhihu.com/p/712769314

我进一步解释:

有环有死锁:

两个环,都是死锁

有环无死锁:

虽然有环(R1和R2形成循环等待关系),但是R1可以通过申请P2来避免循环等待

死锁不是环导致的,而是形成环路+在环路上等待(申请失败就会等待)的问题(两个条件必须同时满足,这个要强调!)导致的,很显然图中R1除了申请P3资源,还能申请另一个P2资源,这样就有效的规避了环路等待的这个条件

结论: 有环不一定有死锁,死锁不是环导致的,而是形成环路+在环路上等待的问题导致的

5.解决死锁的方法

"产生死锁 → 四个条件同时满足"的逆否命题是"四个条件不同时满足 → 不产生死锁",也就是说: 破坏一个条件就一定不可能有死锁

破坏互斥条件

代码要重写,难度大,一般不可能

破坏请求与保持条件

一个执行流因请求资源而阻塞时,释放已获得的资源

破坏不剥夺条件

抢别人的锁,换句话说,就是释放对方的锁

破坏循环等待条件

举例,原来有锁1,锁2,现在线程1先申请锁1再申请锁2,线程2先申请锁2再申请锁1,如下图的交叉申请:

交叉申请容易导致死锁,让两个线程按顺序申请:线程1和线程2同时申请同一把锁,如下图:

得出避免死锁的其中一种方法: 加锁顺序一致

避免死锁的方法

破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配

6.避免死锁算法

死锁检测算法

根据循环等待条件,死锁检测可以利用图算法,检测有向图是否有环

银行家算法

银行家算法Banker's Algorithm

4.结论

如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个函数被重入,若锁还未释放则会产生死锁

5.补充: 线程不能自己等自己,否则死锁

之前在OS72.【Linux】__thread、分离线程中讲过线程不能自己等待自己,否则发生死锁,本文来解释

#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
#include <cstring>
void *thread_routine(void*)
{   
    return nullptr;
}
 
int main() 
{   
    pthread_t tid;
    pthread_create(&tid, nullptr,thread_routine, nullptr);
    sleep(1);
    int ret;
    if (ret=pthread_join(pthread_self(),nullptr))
    {
        std::cout<<"主线程等待失败: "<<strerror(ret)<<std::endl;
    }
    return 0;
}

运行结果: 死锁

解释: 线程正在运行,未退出,但线程使用pthread_join等待自己退出,会死锁,类似鸡生蛋、蛋生鸡的问题

man手册说明

手册里面提到了线程自己等待自己会死锁

man pthread_join

or  thread specifies the calling thread.

这里the calling thread的calling为主动形式,翻译为调用线程,线程指定调用线程,就是线程指定自己

glibc-2.43源码说明

pthread_join在glibc-2.43中等同于___pthread_join,定义在/nptl/pthread_join.c中

int
___pthread_join (pthread_t threadid, void **thread_return)
{
  return __pthread_clockjoin_ex (threadid, thread_return, 0 /* Ignored */,
				 NULL, true);
}
versioned_symbol (libc, ___pthread_join, pthread_join, GLIBC_2_34);

__pthread_clockjoin_ex函数定义在/nptl/pthread_join_common.c中,做了线程等待自己的判断:

int
__pthread_clockjoin_ex (pthread_t threadid, void **thread_return,
                        clockid_t clockid,
                        const struct __timespec64 *abstime,
                        bool cancel)
{
  if (cancel)
    __pthread_testcancel ();

  struct pthread *pd = (struct pthread *) threadid;

  /* Make sure the clock and time specified are valid.  */
  if (abstime
      && __glibc_unlikely (!futex_abstimed_supported_clockid (clockid)
			   || ! valid_nanoseconds (abstime->tv_nsec)))
    return EINVAL;

  LIBC_PROBE (pthread_join, 1, threadid);

  int result = 0;
  unsigned int state;
  while ((state = atomic_load_acquire (&pd->joinstate))
	 != THREAD_STATE_EXITED)
    {
      struct pthread *self = THREAD_SELF;
      if (pd == self
	  && !cancel_enabled_and_canceled (self->cancelhandling))
	return EDEADLK;

      /* POSIX states calling pthread_join on a non joinable thread is
	 undefined.  However, if PD is still in the cache we can warn
	 the caller.  */
      if (state == THREAD_STATE_DETACHED)
	return EINVAL;

      /* pthread_join is a cancellation entrypoint and we use the same
         rationale for pthread_timedjoin_np.

	 The kernel notifies a process which uses CLONE_CHILD_CLEARTID via
	 a memory zeroing and futex wake-up when the process terminates.
	 The futex operation is not private.  */
      int ret = cancel
	? __futex_abstimed_wait_cancelable64 (&pd->joinstate, state, clockid,
					      abstime, LLL_SHARED)
	: __futex_abstimed_wait64 (&pd->joinstate, state, clockid, abstime,
				   LLL_SHARED);
      if (ret == ETIMEDOUT || ret == EOVERFLOW)
	{
	  result = ret;
	  break;
	}
    }

  void *pd_result = pd->result;
  if (__glibc_likely (result == 0))
    {
      if (thread_return != NULL)
	*thread_return = pd_result;

      /* Free the TCB.  */
      __nptl_free_tcb (pd);
    }

  LIBC_PROBE (pthread_join_ret, 3, threadid, result, pd_result);

  return result;
}

注意while循环中的EDEADLK:

if (pd == self
	  && !cancel_enabled_and_canceled (self->cancelhandling))
	return EDEADLK;

结论: pthread_join(pthread_self(),ptr),线程自己等待自己,自己又不退出,pthread_join会主动判断这个情况,直接错误返回,否则会导致死锁

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐