首页 > 系统相关 >linux之线程互斥(万字长文详解)

linux之线程互斥(万字长文详解)

时间:2024-01-21 13:03:26浏览次数:37  
标签:加锁 函数 lock linux 互斥 mutex pthread 线程

linux之线程互斥

多线程在访问共享资源时的问题

假如我们设置一个全局变量!

int tickets = 1000;//充当有1000张票
void* getTicket(void* args)
{
     std::string username =  static_cast<const char*>(args);
     while(true)
     {
           if(tickets>0)
           {
               usleep(1245);//这个是用来模拟线程等待
               std::cout << username << " 正在抢票" << tickets<<std::endl;
               tickets--;
           }
           else {
               break;
           }
     }
     return nullptr;
}

==让多个线程同时的去执行这个抢票函数——会有有可能出现一个结果,票变为负数!==

image-20230815095553054

像这样

想要看到这个现象就需要尽可能的让多个线程交叉执行!(不要并行)

什么叫做交叉执行——即让调度器尽可能的频繁发生线程调度!这样子就会可能会在访问全局变量的时候出现数据交叉的问题!

线程一般在什么时候发生切换呢?——==时间片到了,来了更高优先级的线程==,或者==线程等待==的时候

==那么线程是在什么时候检测上面的问题呢?——即线程什么时候知道自己要切换的呢?从内核态转换为用户态的时候!线程就要对调度状态进行检测!如果可以就直接发生线程切换!==

为什么会出现这样的情况呢?

我们假设一个最极端的情况——tickets已经是等于1了

==下面这就是发生的线程切换的流程!==

image-20230815101537539

进行自减的逻辑

image-20230815102723409

==因为判断和更新两个是分离的!在判断和更新之间,线程发生了大量的切换!最终可能会发生tickets已经为1了却放了好几个线程同时进入,对这个变量做自减,最终导致了我们的数据出现了负数的情况!==

==而且自减这个行为本身也是不安全的!——为什么呢?虽然看起只有一条语句,但是我们上面说了自减的逻辑是要做三件事情!读取,更改,写回!在汇编中无论是++/--,都是至少有三条语句!分别对应这就是从内存读取数据到CPU中,在寄存器中进行算逻运算(算术逻辑运算),写回新的结果到内存中变量的位置!==

image-20230815105705083

image-20230815110028100

image-20230815110402677

==问题就在这里出现了!我们按理来说两个线程对一个全局变量进行自减——1000应该变成998!可是自减两次却是变成了999==

或者还有一种极端情况!——那就是threadB执行自减了200次,将1000变成了800

而然后切换为了threadA,然后threadA继续从第三步开始执行!——写回内存!那么此时这个800,就会变成999——这种情况就更糟糕了!

==这时候就是多线程运算的时候发生了干扰问题!——所以++/--也是不安全的在多线程中!==

image-20230816152109970

-- 操作并不是原子操作,而是对应三条汇编指令:

load :将共享变量ticket从内存加载到寄存器中

update : 更新寄存器里面的值,执行-1操作 store :将新值,从寄存器写回共享变量ticket的内存地址

==综上我们可以得出一个结论!我们定义的全局变量,在没有保护的时候!往往是线程不安全的!像上面多个线程在交替执行造成的数据安全问题——我们称之为发生了数据不一致问题!==

为了解决问题于是就有了一个解决方案——加锁

进程线程间的互斥相关背景概念

==多个执行流进行安全访问的共享资源——我们称之为临界资源!==

==我们把多个执行流中访问临界资源的代码——我们称之为临界区==

临界区往往都是线程代码很小的部分!大部分的代码都是非临界区!

int tickets = 1000;//充当有1000张票
void* getTicket(void* args)
{
    std::string username =  static_cast<const char*>(args);
    while(true)
    {
        if(tickets>0)
        {
            usleep(1245);//这个是用来模拟线程等待
     ////////////////////////////////////
            std::cout << username << " 正在抢票" << tickets<<std::endl;
            tickets--;//只有这两行代码访问了共享资源!
            //加了保护之后也就只有这两行代码属于临界区
    /////////////////////////////////////
        }
        else {
            break;
        }
    }
    return nullptr;
}

==我们想要让多个线程串行访问共享资源——这就是互斥==

==对一个资源进行访问的时候,要么不做!要么做完!——我们将这个特点称之为原子性==

像是上面的进行tickets--,在进行到一半的时候就被切换走了——这种就是非原子性的!

因为这个行为没有彻底做完!——存在了还没做完的一个中间状态

==原子性只有,没做和做完这两种状态!==

方便理解我们可以具体点的来说——==一个对资源进行的操作,如果只用了一条汇编就能完成,那就是原子性的!==(但是这不完全是对的!我们只能说一条汇编能完成的就是原子,但不代表不是一条就不是原子,也有这种情况,但是上面的++/--就不是属于这种情况)

互斥锁

我们说过为了解决多线程访问共享资源的问题于是就有了一个解决方案——加锁

那么锁是什么呢?

image-20230815113224253

我们可以看到是一个pthread_mutex_t的数据类型——==这就是锁==

image-20230815165955904

就是一个联合体

有一个专门用来初始化这个锁的函数pthread_mutex_init

第一个参数就是一个输出型参数——将我们定义的锁传进去,进行初始化!

还有一个专门释放这个锁的函数pthread_mutex_destroy

该函数只有一个参数!那就是要释放的锁的地址!

==有了所我们就要用这个锁!——那么就要加锁和解锁!==

image-20230815164843523

我们要使用的使用的时候就用pthread_mutex_lock进行加锁!

我们不想使用的试试就用pthread_mutex_unlock进行解锁!

==这样子我们就可以通过对共享资源进行加锁实现共享资源临界资源化!==

int tickets = 1000;//充当有1000张票
class ThreadData
{
public:
    ThreadData(const std::string &threadname,pthread_mutex_t* mutex_p)
        : threadname_(threadname),
        mutex_p_(mutex_p)
    {}

public:
    std::string threadname_;
    pthread_mutex_t* mutex_p_;
};
void* getTicket(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    while(true)
    {
         pthread_mutex_lock(td->mutex_p_);
        if(tickets>0)
        {
            usleep(1245);
            std::cout << td->threadname_ << " 正在抢票" << tickets<<std::endl;
            tickets--;
            pthread_mutex_unlock(td->mutex_p_);
        }
        else {
            pthread_mutex_unlock(td->mutex_p_);
            //满足了它会加锁后解锁!
            //不满足也要加锁后解锁!
            //如果不解锁后果很严重
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_mutex_t lock;//只要是一把公共的锁!
    //那么我们就可以使用这个公共的锁对临界资源进行加锁!
    pthread_mutex_init(&lock,nullptr); //初始化这个锁
#define NUM 4
    std::vector<pthread_t> tids(NUM);
    for (int i = 0; i < NUM; i++)
    {
        char buffer[64];
        snprintf(buffer,sizeof buffer,"thread %d",i);
        ThreadData *td = new ThreadData(buffer,&lock);
        pthread_create(&tids[i],nullptr,getTicket,td);
    }
    for(const auto &tid:tids)
    {
        pthread_join(tid,nullptr);
    }

    pthread_mutex_destroy(&lock);//不使用后销毁
}

image-20230815172523821

这样子我们可以看到就不会出现出现数据出现负数的问题了!——但是为什么只有一个线程在抢?而且运行速度变慢了!

==加锁和解锁的过程是多个线程串行执行的!所以这就是为什么程序变慢了!==

==那为什么只有一个线程一直在运行呢?——因为当第一个线程抢到锁之后,其他线程就进入休眠!等到第一个线程执行完毕解锁之后!然后在解锁后,第一个线程又立马抢到锁了!其他线程没有抢到!那么就只能每一次都让第一个线程去运行!==

互斥锁只规定互斥访问!没有规定必须让谁去优先执行!——只要谁的竞争能力强!那么谁就能一直持有锁!

所以锁就是让多个执行流竞争的结果!——因为第一个线程竞争锁的能力更强,所以他就能一直持有锁!

void* getTicket(void* args)
{
    // std::string username =  static_cast<const char*>(args);
    ThreadData* td = static_cast<ThreadData*>(args);
    while(true)
    {

        pthread_mutex_lock(td->mutex_p_);
        if(tickets>0)
        {
            usleep(1245);
            std::cout << td->threadname_ << " 正在抢票" << tickets<<std::endl;
            tickets--;
            pthread_mutex_unlock(td->mutex_p_);
        }
        else {
            pthread_mutex_unlock(td->mutex_p_);
            break;
        }
        // 实际中抢票也不只是--,就完毕了!还有其他事情要做!
        // 这是只有在这段时间里面!其他线程才有可能拿到锁!
        // 所以我们用usleep来模拟,抢完票后处理其他事情的时间
        usleep(1231); // 例如:形成订单给用户
    }
    return nullptr;
}

image-20230815173906402

==除了上面的那种加锁方式之外!我们还有其他的加锁方式——例如将锁定义为静态的,或者全局的!==

一旦这把锁被定义为了全局的!那么我们就不用使用数pthread_mutex_init进行初始化和pthread_mutex_destroy进行销毁

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//只要这样初始化

==然后就可以直接使用了!==

#include<iostream>
#include<pthread.h>
#include<cstdio>
#include<string>
#include<cstring>
#include<unistd.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//全局变量
int tickets = 1000;
class ThreadData
{
    public:
       ThreadData(const std::string &threadname,pthread_mutex_t* mutex_p)
           : threadname_(threadname),
       mutex_p_(mutex_p)
       {}

    public:
       std::string threadname_;
       pthread_mutex_t* mutex_p_;
};
void* getTicket(void* args)
{
       std::string username =  static_cast<const char*>(args);
       while(true)
       {

           pthread_mutex_lock(&lock);//直接进行加锁和解锁
           if(tickets>0)
           {
               usleep(1245);
               std::cout << username << " 正在抢票" << tickets<<std::endl;
               tickets--;
               pthread_mutex_unlock(&lock);
           }
           else {
               pthread_mutex_unlock(&lock);
               break;
           }
           usleep(1231);
       }
       return nullptr;
}
int main()
{
       pthread_t t1,t2,t3,t4;
       pthread_create(&t1,nullptr,getTicket,(void*)"thread 1");
       pthread_create(&t2,nullptr,getTicket,(void*)"thread 2");
       pthread_create(&t3,nullptr,getTicket,(void*)"thread 3");
       pthread_create(&t4,nullptr,getTicket,(void*)"thread 4");

       pthread_join(t1,nullptr);
       pthread_join(t2,nullptr);
       pthread_join(t3,nullptr);
       pthread_join(t4,nullptr);

}

如何看待互斥锁?

我们访问临界资源之前!我们肯定就是要访问这把锁!——因为我们要保护共享资源!

那么每一个线程肯定也要看到这把锁!——==所以互斥锁本身就是一个共享资源!==

全局的变量是要被保护的!——锁是用来保护全局的资源的!锁本身也是一个全局的资源!

==那么锁的安全的谁来保护呢?——锁是共享资源!也天然是一种临界资源!==

==我们使用phtread_lock函数进行加锁,使用pthread_unlock进行解锁。所以就必须保证加锁解锁的过程是安全的!==——加锁和解锁的过程都是原子的!所以我们不用担心

==如果申请锁成功,就继续向后执行!如果申请锁暂时没有成功,那么执行流会如何呢?==

void* getTicket(void* args)
{
       std::string username =  static_cast<const char*>(args);
       while(true)
       {
           pthread_mutex_lock(&lock);//直接进行加锁和解锁
           pthread_mutex_lock(&lock);//在这里多一次加锁
           //...
       }
       return nullptr;
}
int main()
{
       pthread_t t1,t2,t3,t4;
       pthread_create(&t1,nullptr,getTicket,(void*)"thread 1");
       pthread_create(&t2,nullptr,getTicket,(void*)"thread 2");
       pthread_create(&t3,nullptr,getTicket,(void*)"thread 3");
       pthread_create(&t4,nullptr,getTicket,(void*)"thread 4");

       pthread_join(t1,nullptr);
       pthread_join(t2,nullptr);
       pthread_join(t3,nullptr);
       pthread_join(t4,nullptr);

}

image-20230815201407135

我们可以看到什么都不执行了!——所有的进程都处于休眠的状态!

==所以如果申请暂时没有成功!那么执行流会阻塞!——直到这个锁被释放了!我们的系统才会去唤醒这个线程继续向后运行!,我们将这种锁称为挂起等待锁,四个线程都是处于阻塞是因为,拿到锁的那个线程又申请的一次锁!自己已经拿了一次锁后,又申请自然拿不到了!所以就阻塞了,后面的三个线程压根拿到过锁!所以也阻塞了!==

pthread_mutex_trylock

我们还有一个加锁的函数,就是pthread_mutex_trylock

image-20230815202131326

这个函数的特点就是如果加锁失败了,不会去阻塞等待!而是会出错返回!这也是一种非阻塞加锁的方式

image-20230815202157964

成功返回0,失败则返回错误码!

==谁持有锁谁才能进入临界区!==

加锁后能切换线程吗?

image-20230815205818232

image-20230816152731974

==如上图未来我们使用锁的时候!一定要尽量的保证临界区的粒度要非常的小!==

比如我们使用锁的时候

void* getTicket(void* args)
{

       //可以这样加锁
       std::string username =  static_cast<const char*>(args);
       while(true)
       {
           pthread_mutex_lock(&lock);//直接进行加锁和解锁
           if(tickets>0)
           {
               usleep(1245);
               std::cout << username << " 正在抢票" << tickets<<std::endl;
               tickets--;
               pthread_mutex_unlock(&lock);
           }
           else {
               pthread_mutex_unlock(&lock);
               break;
           }
           usleep(1231);
       }
       return nullptr;
}
//也可以这样加锁!
void* getTicket(void* args)
{
       pthread_mutex_lock(&lock);//直接进行加锁和解锁
       std::string username =  static_cast<const char*>(args);
       while(true)
       {
           if(tickets>0)
           {
               usleep(1245);
               std::cout << username << " 正在抢票" << tickets<<std::endl;
               tickets--;
           }
           else {
               break;
           }
           usleep(1231);
       }
       pthread_mutex_unlock(&lock);
       return nullptr;
}

粒度就是指——锁中间保护的代码的个数的多少!

因为我们一加锁,就式串行的!不能并行执行!这样子效率就变低了!==所以我们要尽量让临界区变得很短!==将非临界区资源的代码这种可放可不放的,都放在临界区外面!

这样子让多执行流并发执行的时候,让关键的代码安全的访问!然后也能高效率的运行!否则速度就会变的很慢

==加锁这个行为必须做到要加就都加上!不能一个加一个不加!这叫做写代码有bug!==

加锁/解锁的流程

加锁的过程是原子的!解锁也是原子的(但是相对于解锁安全性要去没有那么高!因为一次解锁肯定是只有一个!)

==那么加锁是如何实现原子性的?==

为了实现互斥锁操作,大多数体系结构(例如:x86_32 x86_64或者arm)都提供了swap或exchange指令,==该指令的作用是把寄存器和内存单元的数据相交换==,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。——这是在软件层面实现原子性

==加锁的流程是如下图==

image-20230815221243200

image-20230815223224757

我们发现发现线程B在交换锁的时候0换0,此时线程B进行后续的判断,发现申请锁失败了!——==那么在后续的指令里面线程B只能挂起等待了!==

==其实在线程A完成了xchgb指令后!线程A就已经拿到了锁!——这就是为什么xchgb要求是原子的!==

所以哪怕线程A在执行xchgb指令被后就被切换了!也是不怕的!因为此时锁已经拿到手了!(放进上下文里面了!)

然后判断寄存器al的值是大于0的!所以就会return 0,这样子线程A就可以继续执行后续的代码了!而不是挂起等待!

==关键就在于交换可以使用一条汇编指令来完成!——而交换的本质就是:将共享的数据交换到线程的上文中去!==

接下来只要线程A不把这个数据从上下文中写回lock里面!因为所有的线程都是要在lock里面拿的!那么所有执行流再怎么进行切换!都进不到临界资源当中!——这样子线程A就可以安全的将资源进行访问了!

==解锁的流程如下图!——解锁的过程是很简单!==

image-20230816175123834

==不是把上下文的1mov到lcok里面!而是直接将数字 1 mov到lock!里面!然后就解锁成功了!==

这就是为什么xchgb指令之前要先将0,mov进al寄存器里面!因为里面可能是1

互斥锁的封装

我们想要简单的使用锁!可以对其进行一些简单的设计!——让锁能出作用域后自动的就解锁!

而不是让我们手动的去解锁!

//Mutex.hpp
#pragma once
#include<iostream>
#include<pthread.h>

namespace MYTOOL
{
       class Mutex
       {
       public:
           Mutex(pthread_mutex_t *lock_p)
               : lock_p_(lock_p)
               {
               }
           void lock()
           {
               if(lock_p_) pthread_mutex_lock(lock_p_);
           }
           void unlock()
           {
               if(lock_p_) pthread_mutex_unlock(lock_p_);
           }
           ~Mutex()
           {}
       private:
           pthread_mutex_t *lock_p_;
       };

       class LockGuard
       {
       public:
           LockGuard(pthread_mutex_t *mutex)
               : mutex_(mutex)
               {
                   mutex_.lock();
               }

           ~LockGuard()
           {
               mutex_.unlock();
           }
       private:
           Mutex mutex_;
       };
}
#include<iostream>
#include<unistd.h>
#include"Mutex.hpp"
void* getTicket(void* args)
{
       std::string username =  static_cast<const char*>(args);
       while(true)
       {
           MYTOOL::LockGuard lockguard(&lock);
           //当构建这个锁的时候会自动的调用构造函数去加锁!只要出来while循环的作用域!那么这个锁就会自动调用析构函数解锁!        
           ///////////////////////////////////////////////////////////
           if(tickets>0)
           {
               usleep(1245);
               std::cout << username << " 正在抢票" << tickets<<std::endl;
               tickets--;
           }
           else {
               break;//我们也不怕提前break因为只要出了while的作用域,那么这个锁就会自动的解锁!
           }
           usleep(1231);
           ///////////////////////////////////////////////////////////////
           //这些代码都是属于加锁的范围!
       }
       return nullptr;
}

//但是我们上面这样子其实让不需要加锁的代码也加了锁!——例如usleep
void* getTicket(void* args)
{
       std::string username =  static_cast<const char*>(args);
       while(true)
       {
        
           {
               MYTOOL::LockGuard lockguard(&lock);
               //我们可以只需要加锁的临界资源区上下加一个花括号!创建一个代码块
               //这样子出了这个代码块!那么锁就自动的解锁了!
               if(tickets>0)
               {
                   usleep(1245);
                   {
                       //我们可以只需要加锁的临界资源区上下加一个花括号!
                       //这样子出了这个代码块!那么锁就自动的解锁了!
                       std::cout << username << " 正在抢票" << tickets<<std::endl;
                       tickets--;
                   }
               }
               else 
               {
                   break;
               }
           }
        
           usleep(1231);
       }
       return nullptr;
}

可重入和线程安全

概念

==线程安全==:多个线程并发同一段代码时,不会出现不同的结果——这就是线程安全。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。——这就是线程不安全

==重入==:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数。

像是我们上面写的getTicket函数就是可重入函数!

==我们有很多的线程安全问题其实就是因为不可重入函数导致的!==

常见的线程不安全的情况

不保护共享变量的函数

例如:全局变量

函数状态随着被调用,状态发生变化的函数

例如:函数里面有个静态变量!会导致函数状态发生改变!

返回指向静态变量指针的函数

调用线程不安全函数的函数

常见线程安全的情况

每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的

类或者接口对于线程来说都是原子操作

多个线程之间的切换不会导致该接口的执行结果存在二义性

举个例子:我们写个函数,函数里面全是局部变量!没有任何全局变量或者静态变量!那么这个代码块就是线程安全的!

常见不可重入的情况

调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的

调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构

可重入函数体内使用了静态的数据结构

==线程安全和不可重入是两个概念,分别对应的主体是线程和函数!==

如果一个全局变量被没有保护的访问!这个对于线程来说就是线程不安全的!

如果这个全局变量还恰好属于一个函数,而且这个函数被重进入了!——那么这个函数就是不可重入函数!

==所以线程不安全,可能是由不可重入导致的!也有可能是函数内部使用了全局数据,从而导致了线程不安全==

常见可重入的情况

不使用全局变量或静态变量

不使用用malloc或者new开辟出的空间

不调用不可重入函数

不返回静态或全局数据,所有数据都有函数的调用者提供

使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

函数是可重入的,那就是线程安全的

函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别

可重入函数是线程安全函数的一种

==线程安全不一定是可重入的,而可重入函数则一定是线程安全的。==

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

死锁

==死锁是指一组执行流,在持有自己锁资源的同时,还去申请对方的锁资源!因为锁是不可抢占的锁(即除非自己主动归还,否则无论对方怎么样都无法拿到),所以就有可能出现多执行流互相等待对方的资源!从而导致代码无法推进的情况!==

举个例子:有两个小朋友一个小A,一个小B,一起去一个商店买东西,这两个小朋友的父母各自个给了他们5毛钱的零花钱,他们想去买棒棒糖,一个棒棒糖一块钱

于是小A向小B借它的5毛钱,买棒棒糖

小B不同意,反问小A,为什么不把他的5毛钱借给他,它也想买棒棒糖

这两位小朋友互相拿着自己的5毛钱,还在要这对方的5毛钱,两个人争执不下,结果就是谁也没有买棒棒糖——这种状态就是死锁!

上面我们说死锁是发生在==有多把锁的场景下==,我们持有自己的锁不放,还要对方的锁,对方也是如此,那么就容易造成死锁!

==那么一把锁,可能造成死锁吗?——可能!==

我们持有一把锁!然后我们忘记了自己有一把锁!然后我们过来很长时间又跑去申请这把锁了!——那么就有可能造成死锁!所以这不是不可能!

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int main()
{
       pthread_mutex_lock(&lock);
       //........
       //中间写了例如5w行代码!
       pthread_mutex_lock(&lock);
       return 0;
}

==这就会死锁了!==而且这种自己把自己挂起,就没有办法释放锁了!

==为什么会有死锁?==

首先一定是我们用了锁!——> 为什么我们要用锁?——>因为我们要保证临界资源的安全——>为什么我们要保护临界资源的安全呢?——>因为多线程访问可能会出现数据不一致问题!——>为什么会有数据不一致问题呢?——>因为我们是多线程,访问了全局资源!——>为什么多线程访问全局资源就会造成这个问题呢?——>因为多线程的大部分资源(全局的)是共享的!——>共享是多线程的特性!

==然后我们按照这个逻辑链反过来想,线程存在是为了解决进程不易通信的问题,但是这引入了一个新的问题,就是数据不一致!为了解决数据不一致,所以引入了锁!引入了锁后于是就有了死锁!==

任何技术都是有自己的边界,是解决问题的!但是在解决问题的同时,一定会可能引入新的问题!

==死锁的四个必要条件==

  • 互斥条件——一个资源每次只能被一个执行流使用

  • 请求与保持条件——一个执行流因请求资源而阻塞时,对已获得的资源保持不放

请求就是我要你的!保持就是我不释放我的

  • 不剥夺条件——一个执行流已获得的资源,在末使用完之前,不能强行剥夺

就是说不能通过某些条件,例如优先级,手动设置状态,允许竞争抢别人的锁!

如果允许就是剥夺

  • 环路等待条件——:若干执行流之间形成一种头尾相接的循环等待资源的关系

A有自己的锁,去要B的锁!

B有自己的锁,去要C的锁!

C有自己的搜,去要A的锁!

这就是一种环路等待!——然后彼此都释放自己的锁

环路等待条件和请求与保持条件区别在于:

请求与保持更强调:我要你的锁,但是我不释放我的锁这个行为!

环路等待更强调:是因为前面三个条件,从而导致的,你要我的锁,我不给,我要你的锁,你不给这个环路问题

==满足这四个条件——那么就一定是死锁!==

==如果破坏死锁==

只要我们破坏上面的四个必要条件中的一个!我们就能破坏死锁!

  • 互斥,这个是锁的特性!我们没有办法破坏这个条件

  • 请求与保持这个条件该怎么破坏?

比如说我们线程要访问多个临界资源,需要同时持有两把锁

==申请一把锁成功后,再申请第二把锁,但是失败了(使用trylock),那么我们就立刻释放掉我们曾经持有的锁,不进行请求和保持!那么这样子我们就不会造成死锁了!==

所以这个条件就很好破坏

  • 不剥夺条件

剥夺就是不能去抢,我们可以去设置一个竞争策略,一个线程申请锁,如果申请到A锁,接下来要申请B锁!但是B锁被其他线程拿到了!

==那么我们就可以去比较,例如:两个线程的优先级,或者一些状态==

==持有A锁的优先级更高,于是让持有B锁的线程主动释放锁==

  • 环路等待

环路等待就是A线程先申请A锁,再申请B锁,B线程先申请B锁,再申请A锁

==两线程申请锁的顺序就是相反的!我们就不要让这种情况出现!让其申请锁的顺序保持一致!就可以破坏环路等待问题!==

==避免死锁具体方案==

  1. 破坏死锁的四个必要条件

  2. 加锁顺序一致——避免环路等待

  3. 避免锁未释放的场景 ——避免请求与保持,方式锁为释放的场景

  4. 资源一次性分配

防止出现第一个锁在首行代码,第二个锁在第1w行代码

如果有5个地方要加锁!那么就在最开始一次性申请出5个锁,不要将这5个申请分布在各个代码区里面!

==避免死锁算法==

  • 死锁检测算法

==首先我们有一个问题——一个线程申请锁,能否由另一个线程来解锁呢?==

我们可以回顾一下上面的解锁的汇编伪代码!——解锁只需要将1放入Mutex所在的内存块里面==是没有要求这个1是来自持有锁的上下文的!==

那么答案就很明显了——==可以的!==

现在我们有一个场景:

有一份公共资源,有一批锁和一部分代码!

多线程在运行的时候,其他线程都在正常的执行业务逻辑!有一个线程则是观察其他线程有没有正常的推进

这个是如何实现的呢?——定义一个类,类里面有一个锁相关的计数器,这个计算器是衡量每一个线程是否正常运算,只要运行了就将计数器++。然后一个线程就专门来盯着这个资源,如果某一个锁的计数器长时间没有发生变化!那么就预测这个线程是死锁了!那么就将这线程对应的锁unlock掉!这样子就可以解决死锁问题

==这就是死锁检查算法==

  • 银行家算法(可以去了解)

标签:加锁,函数,lock,linux,互斥,mutex,pthread,线程
From: https://blog.51cto.com/u_15835985/9354706

相关文章

  • 【深入浅出JVM原理及调优】「搭建理论知识框架」全方位带你深度剖析Java线程转储分析
    专栏介绍学习JVM需要一定的编程经验和计算机基础知识,适用于从事Java开发、系统架构设计、性能优化、研究学习等领域的专业人士和技术爱好者。前提准备编程基础:具备良好的编程基础,理解面向对象编程(OOP)的基本概念,熟悉Java编程语言。数据结构与算法:对基本的数据结构和算法有一定了解,理......
  • 45个经典Linux面试题!赶紧收藏!
    问题一:绝对路径用什么符号表示?当前目录、上层目录用什么表示?主目录用什么表示?切换目录用什么命令?答案:绝对路径:如/etc/init.d当前目录和上层目录:./../主目录:~/切换目录:cd问题二:怎么查看当前进程?怎么执行退出?怎么查看当前路径?答案:查看当前进程:ps执行退出:exit查看当前路径:pwd问题三......
  • Linux 系统中 $* 和 $@的区别和联系
     001、两者都可以表示shell脚本的所有参数,两者没有差异(不管是否增加双引号) 举例:a、不加双引号[root@PC1test1]#ls##准备了两个测试脚本a.shb.sh[root@PC1test1]#cata.sh##a.sh的内容如......
  • Linux内核accept系统调用源码分析
    内核版本:Linux3.10内核源码地址:https://elixir.bootlin.com/linux/v3.10/source(包含各个版本内核源码,且网页可全局搜索函数)一、应用层-accept()函数/***sockfd:监听socket的文件描述符*addr:存放地址信息的结构体的首地址(用来保存客户端的IP、Port)*addrlen:存放地......
  • 线程池最佳实践!这几个坑使用不当直接生产事故!!
    拿来即用!这篇文章我会介绍我使用线程池的时候应该注意的坑以及一些优秀的实践。1、正确声明线程池线程池必须手动通过 ThreadPoolExecutor 的构造函数来声明,避免使用Executors 类创建线程池,会有OOM风险。Executors 返回线程池对象的弊端如下(后文会详细介绍到):FixedThreadPo......
  • 16-Linux进程管理
    进程的概念:进程是正在执行的一个程序或命令,每一个进程都是一个运行的实体,都有自己的地址空间,并占用一定的系统资源。命令ps:查看当前系统进程状态语法:ps【选项】选项:小技巧:如果想查看进程的CPU占用率和内存占用率,可以使用aux;如果想查看进程的父进程ID可以使用ef案......
  • 17-Linux系统定时任务
    crontab服务管理注意点使用前先确认crontab的守护进程crond是否是打开的状态,一般是开机自启的。[root@192mnt]#systemctlstatuscrond#查看crond进程是否开启。当前是开启的●crond.service-CommandSchedulerLoaded:loaded(/usr/lib/systemd/system/crond.ser......
  • 18-Linux软件包管理
    RPM介绍RPM(RedHatPackageManager),RedHat软件包管理工具,类似windows里面的setup.exe是Linux这系列操作系统里面的打包安装工具,它虽然是RedHat的标志,但理念是通用的。RPM包的名称格式:Apache-1.3.23-11.i386.rpm。其中:“apache”软件名称“1.3.23-11”软件的版本号,主版本和此......
  • 19-Linux克隆虚拟机
    从现有虚拟机(关机状态)克隆出新虚拟机,右键选择管理=>克隆  点击下一步  选择虚拟机中的当前状态  选择创建完整克隆  设置虚拟机名称及存储位置等待克隆完成 ......
  • Linux常用命令
    性能监控(cpu内存磁盘网络)性能监控命令 uptime:显示系统平均负载以及系统启动时间查看CPU mpstat查看内存 vmstat15每秒刷新一次刷5次查看磁盘 ioiostat-x15查看网络 iftop查看进程资源占用 ......