0


Linux线程安全

上篇博客我们谈到了线程概念,线程与进程,线程控制以及线程地址空间等问题,这篇博客我们继续介绍线程的话题。

1. Linux线程互斥

1.1 进程线程间的互斥概念与进入

临界资源: 多线程执行流共享的资源叫做临界资源。
临界区: 每个线程内部,访问临界资源的代码,就叫做临界区。
互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性: 不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

临界资源和临界区

进程之间如果要进行通信我们需要先创建第三方资源,让不同的进程看到同一份资源,由于这份第三方资源可以由操作系统中的不同模块提供,于是进程间通信的方式有很多种。(管道和system VIPC)进程间通信中的第三方资源就叫做临界资源,访问第三方资源的代码就叫做临界区。

而多线程的大部分资源都是共享的,线程之间进行通信不需要费那么大的劲去创建第三方资源。

下面我们写一个抢票程序,同时我们可以对pthread_t进行封装。如下:

Thread.hpp:

#ifndef __THREAD_HPP__
#define __THREAD_HPP__
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <functional>

namespace ThreadModule
{
    template <typename T>
    using func_t = std::function<void(T &)>;
    // std::function 是一个通用多态的函数封装器 返回值void,形参T&
    //  typedef std::function<void(const T&)> func_t; using相当于typedef
    template <typename T>
    class Thread
    {
    public:
        Thread(func_t<T> func, T &data, const std::string &name = "none-name")
            : _func(func), _data(data), _threadname(name), _stop(true)
        {
        }
        ~Thread()
        {
        }
        void Excute()
        {
            _func(_data);
        }
        static void *threadroutine(void *args) // 类成员函数形参有this指针,会造成传入参数过多
        {
            Thread<T> *self = static_cast<Thread<T> *>(args); // 转为当前对象的指针
            self->Excute();
            // err调用  _func(_data);   //非静态成员函数 必须通过对象进行调用
            return nullptr;
        }
        bool Start()
        {
            // 启动线程,执行对应方法 ,构造函数中只是创建了线程对应的名字,要执行的方法
            int n = pthread_create(&_tid, nullptr, threadroutine, this);
            if (!n)
            {
                _stop = false;
                return true;
            }
            else
            {
                return false;
            }
        }

        void Join()
        {
            if (!_stop)
            {
                pthread_join(_tid, nullptr);
            }
        }
        std::string name()
        {
            return _threadname;
        }
        void Stop()
        {
            _stop = true;
        }

    private:
        pthread_t _tid;          // create时自动放入tid中 
        std::string _threadname; // 线程名字
        T &_data;                // 为了让所有的线程访问同一个全局变量
        func_t<T> _func;         // 传入啥函数就调用啥函数
        bool _stop;
    };
}
#endif

Thread_test.cpp:

#include <iostream>
#include <vector>
#include "Thread.hpp"

using namespace ThreadModule;

int g_tickets = 10000; // 共享资源,没有保护的,产生数据不一致问题

void route(int &tickets)
{
    while (true)
    {
        if (tickets > 0)
        {
            usleep(1000);
            printf("get tickets: %d\n", tickets);
            tickets--;
        }
        else
        {
            break;
        }
    }
}
const int thread_num = 4;

int main()
{
    // std::cout << "main: &tickets: " << &g_tickets << std::endl;

    std::vector<Thread<int>> threads;
    // 1. 创建一批线程
    for (int i = 0; i < thread_num; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        threads.emplace_back(route, g_tickets, name); // 传入线程名,访问变量,执行方法
                                                      // 操作数据在类内部,内部再封装一个routeine,回调func(data)
    }

    // 2. 启动 一批线程
    for (auto &thread : threads)
    {
        thread.Start();
    }

    // 3. 等待一批线程
    for (auto &thread : threads)
    {
        thread.Join();
        std::cout << "wait thread done, thread is: " << thread.name() << std::endl;
    }
    return 0;
}
// void* TicketGrabbing(void* arg) if (tickets > 0){}
//  pthread_create(&t1, NULL, TicketGrabbing, "thread 1");

可是,当我们运行代码,却发现票数竟然抢到了负数。所有线程共享的票属于是临界资源,printf和ticket --就叫做临界区,因为代码对临界资源进行了访问而不加以保护,造成数据不一致。

剩余票数出现负数的原因:

  • if语句判断条件为真以后,代码可以并发的切换到其他线程。
  • usleep用于模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
  • --ticket操作本身就不是一个原子操作。

为什么

--ticket

不是原子操作?

我们对一个变量进行

--

,被编译器编译后本质上是三行汇编,++也类似,执行三种操作。

  1. load:将共享变量tickets从内存加载到寄存器中。

  2. update:更新寄存器里面的值,执行-1操作。

  3. store:将新值从寄存器写回共享变量tickets的内存地址。

     在多线程情况下,如果这多个**执行流都自顾自的对临界资源进行操作,那么此时就可能导致数据不一致**的问题。解决该问题的方案就叫做**互斥**,互斥的作用就是,保证在任何时候有且只有**一个执行流进入临界区对临界资源**进行访问,同时执行流对临界资源的访问具有原子性,它不可被打断,访问要么成功要么失败。
    

1.2 互斥量mutex

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量成为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发操作共享变量,就会带来上面问题。

要解决上述抢票系统的问题,需要做到三点:

  1. 代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  2. 如果多个线程同时请求执行临界区的代码,并且此时临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
    要做到这三点,本质上就是需要一把锁,Linux上提供的这把锁叫互斥量。通过这把锁,进入一个房间,房间只允许一个人进入,同时出门的时候必须把钥匙挂在房间外面的墙上。

mutex互斥量接口

互斥量的定义销毁接口

两种方式:

第一种:

pthread_mutex_t   mutex = PTHREAD_MUTEX_INITIALIZER;

这种方式下的mutex变量需要定义成全局的或者静态局部的,且不用destroy.

第二种:

int main() 
{
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);
    //业务逻辑
    pthread_mutex_destroy(&mutex);
}
  • 互斥量初始化成功返回0,失败返回错误码。这是pthread的库的标准返回的两种情况
  • 互斥量destroy 成功返回0,失败返回错误码。与上面一样

互斥量的锁操作

加锁和解锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

例如,我们在上述的抢票系统中引入互斥量,每一个线程要进入临界区之前都必须先申请锁,只有申请到锁的线程才可以进入临界区对临界资源进行访问,并且当线程出临界区的时候需要释放锁,这样才能让其余要进入临界区的线程继续竞争锁。

void route(ThreadData *td)
{
    while (true)
    {
        { 
            pthread_mutex_lock(&td->_mutex);  //加锁
            // std::lock_guard<std::mutex> lock(td->_mutex);
            if (td->_tickets > 0) // 1
            {
                usleep(1000);
                printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets); // 2
                td->_tickets--;                                                           // 3
                td->_total++;
                pthread_mutex_unlock(&td->_mutex);//解锁
            }
            else
            {
                pthread_mutex_unlock(&td->_mutex);//解锁
                break;
            }
        }
    }
}

之后运行:

注意:

  • 在大部分情况下,加锁本身都是有损于性能的事,它让多执行流由并行执行变为了串行执行,这几乎是不可避免的。
  • 我们应该在合适的位置进行加锁和解锁,这样能尽可能减少加锁带来的性能开销成本。
  • 进行临界资源的保护,是所有执行流都应该遵守的标准,这时程序员在编码时需要注意的。

互斥量实现原理

加锁后的原子性体现在哪里?

引入互斥量后,当一个线程申请到锁进入临界区时,在其他线程看来该线程只有两种状态,要么没有申请锁,要么锁已经释放了,因为只有这两种状态对其他线程才是有意义的。

例如,图中线程1进入临界区后,在线程2、3、4看来,线程1要么没有申请锁,要么线程1已经将锁释放了,因为只有这两种状态对线程2、3、4才是有意义的,当线程2、3、4检测到其他状态时也就被阻塞了。

此时对于线程2、3、4而言,它们就认为线程1的整个操作过程是原子的。

临界区内的线程可能进行线程切换吗?

临界区内的线程完全可能进行线程切换,但即便该线程被切走,其他线程也无法进入临界区进行资源访问,因为此时该线程是拿着锁被切走的,锁没有被释放也就意味着其他线程无法申请到锁,也就无法进入临界区进行资源访问了。

其他想进入该临界区进行资源访问的线程,必须等该线程执行完临界区的代码并释放锁之后,才能申请锁,申请到锁之后才能进入临界区。

锁是否需要被保护?

我们说被多个执行流共享的资源叫做临界资源,访问临界资源的代码叫做临界区。所有的线程在进入临界区之前都必须竞争式的申请锁,因此锁也是被多个执行流共享的资源,也就是说锁本身就是临界资源。

既然锁是临界资源,那么锁就必须被保护起来,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?

锁实际上是自己保护自己的,我们只需要保证申请锁的过程是原子的,那么锁就是安全的。

如何保证申请锁的过程是原子的?

上面我们已经说明了--和++操作不是原子操作,可能会导致数据不一致问题。
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用就是把寄存器和内存单元的数据相交换。
由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期,因为系统总线只有一套。

下面我们来看看lock和unlock的伪代码:

我们可以认为共享变量 mutex的初始值为1,al是计算机中的一个寄存器,当线程申请锁时,需要执行以下步骤:

  1. 先将al寄存器中的值清0。该动作可以被多个线程同时执行,因为每个线程都有自己的一组寄存器(上下文信息),执行该动作本质上是将自己的al寄存器清0。
  2. 然后交换al寄存器和mutex中的值。xchgb是体系结构提供的交换指令,该指令可以完成寄存器和内存单元之间数据的交换。
  3. 最后判断al寄存器中的值是否大于0。若大于0则申请锁成功,此时就可以进入临界区访问对应的临界资源;否则申请锁失败需要被挂起等待,直到锁被释放后再次竞争申请锁。

例如,此时内存中mutex的值为1,线程申请锁时先将al寄存器中的值清0,然后将al寄存器中的值与内存中mutex的值进行交换。

交换完成后检测该线程的al寄存器中的值为1,则该线程申请锁成功,可以进入临界区对临界资源进行访问。

而此后的线程若是再申请锁,与内存中的mutex交换得到的值就是0了,此时该线程申请锁失败,需要被挂起等待,直到锁被释放后再次竞争申请锁。

当线程释放锁时,需要执行以下步骤:

  1. 将内存中的mutex置回1。使得下一个申请锁的线程在执行交换指令后能够得到1,形象地说就是“将锁的钥匙放回去”。
  2. 唤醒等待Mutex的线程。唤醒这些因为申请锁失败而被挂起的线程,让它们继续竞争申请锁。

2. Linux同步

同步概念与竞态条件

同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这就叫做同步。
竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件。

  • 首先需要明确的是,单纯的加锁是会存在某些问题的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后什么也不做,所以在我们看来这个线程就一直在申请锁和释放锁,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题。
  • 单纯的加锁是没有错的,它能够保证在同一时间只有一个线程进入临界区,但它没有高效的让每一个线程使用这份临界资源。
  • 现在我们增加一个规则,当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后。
  • 增加这个规则之后,下一个获取到锁的资源的线程就一定是在资源等待队列首部的线程,如果有十个线程,此时我们就能够让这十个线程按照某种次序进行临界资源的访问。

例如,现在有两个线程访问一块临界区,一个线程往临界区写入数据,另一个线程从临界区读取数据,但负责数据写入的线程的竞争力特别强,该线程每次都能竞争到锁,那么此时该线程就一直在执行写入操作,直到临界区被写满,此后该线程就一直在进行申请锁和释放锁。而负责数据读取的线程由于竞争力太弱,每次都申请不到锁,因此无法进行数据的读取,引入同步后该问题就能很好的解决。

条件变量

条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述。

条件变量主要包括两个动作:

  • 一个线程等待条件变量的条件成立而被挂起。pthread_cond_wait
  • 另一个线程使条件成立后唤醒等待的线程。pthread_cond_signal和pthread_cond_broadcast

条件变量通常需要配合互斥锁一起使用。

条件变量接口

初始化条件变量

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

参数说明:

  • cond:需要初始化的条件变量。
  • attr:初始化条件变量的属性,一般设置为NULL即可。

返回值说明:

  • 条件变量初始化成功返回0,失败返回错误码。

调用pthread_cond_init函数初始化条件变量叫做动态分配,除此之外,我们还可以用下面这种方式初始化条件变量,该方式叫做静态分配,静态分配无需销毁

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);

参数说明:

  • cond:需要销毁的条件变量。

返回值说明:

  • 条件变量销毁成功返回0,失败返回错误码。

等待条件变量满足:

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

参数说明:

  • cond:需要等待的条件变量。
  • mutex:当前线程所处临界区对应的互斥锁。

返回值说明:

  • 函数调用成功返回0,失败返回错误码。

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

区别:

  • pthread_cond_signal函数用于唤醒等待队列中首个线程。
  • pthread_cond_broadcast函数用于唤醒等待队列中的全部线程。

参数说明:

  • cond:唤醒在cond条件变量下等待的线程。

使用示例:

#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>

pthread_cond_t gcond = PTHREAD_COND_INITIALIZER; // 条件变量
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER; // 互斥锁

void *SlaverCore(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        // 1. 加锁
        pthread_mutex_lock(&gmutex);
        // 2. 一般条件变量是在加锁和解锁之间使用的
        pthread_cond_wait(&gcond, &gmutex); // gmutex:这个是,是用来被释放的[前一半]
        std::cout << "当前被叫醒的线程是: " << name << std::endl;
        // 3. 解锁
        pthread_mutex_unlock(&gmutex);
    }
}

void *MasterCore(void *args)
{
    sleep(3);
    std::cout << "master 开始工作..." << std::endl;
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        pthread_cond_signal(&gcond);// 唤醒其中一个队列首部的线程
        //pthread_cond_broadcast(&gcond);// 唤醒队列中所有的线程
        std::cout << "master 唤醒一个线程..." << std::endl;
        sleep(1);
    }
}

void StartMaster(std::vector<pthread_t> *tidsptr)
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, MasterCore, (void *)"Master Thread");
    if (n == 0)
    {
        std::cout << "create master success" << std::endl;
    }
    tidsptr->emplace_back(tid);
}

void StartSlaver(std::vector<pthread_t> *tidsptr, int threadnum = 3)
{
    for (int i = 0; i < threadnum; i++)
    {
        char *name = new char[64];
        snprintf(name, 64, "slaver-%d", i + 1); // thread-1
        pthread_t tid;
        int n = pthread_create(&tid, nullptr, SlaverCore, name);
        if (n == 0)
        {
            std::cout << "create success: " << name << std::endl;
            tidsptr->emplace_back(tid);
        }
    }
}

void WaitThread(std::vector<pthread_t> &tids)
{
    for (auto &tid : tids)
    {
        pthread_join(tid, nullptr);
    }
}

int main()
{
    std::vector<pthread_t> tids;
    StartMaster(&tids);
    StartSlaver(&tids, 5);
    WaitThread(tids);
    return 0;
}

奴隶线程按照21354的顺序轮流打印,好像就在排队一样。

为什么pthread_cond_wait需要互斥量

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化,所以一定要用互斥锁来保护,没有互斥锁就无法安全的获取和修改共享数据。
  • 当线程进入临界区时需要先加锁,然后判断内部资源的情况,若不满足当前线程的执行条件,则需要在该条件变量下进行等待,但此时该线程是拿着锁被挂起的,
  • 也就意味着这个锁再也不会被释放了,此时就会发生死锁问题。
  • 所以在调用pthread_cond_wait函数时,还需要将对应的互斥锁传入,此时当线程因为某些条件不满足需要在该条件变量下进行等待时,就会自动释放该互斥锁。
  • 当该线程被唤醒时,该线程会接着执行临界区内的代码,此时便要求该线程必须立马获得对应的互斥锁,因此当某一个线程被另一个线程pthread_cond_signal唤醒时,就会竞争对应的互斥锁,竞争成功pthread_cond_wait进行返回,再执行后面的代码。

总结一下:

  • 等待的时候往往是在临界区内等待的,当该线程进入等待的时候,互斥锁会自动释放,而当该线程被唤醒时,又会自动获得对应的互斥锁。
  • 条件变量需要配合互斥锁使用,其中条件变量是用来完成同步的,而互斥锁是用来完成互斥的。
  • pthread_cond_wait函数有两个功能,一就是让线程在特定的条件变量下等待,二就是让线程释放对应的互斥锁。这两个操作具有原子性。

错误的设计

你可能会想:当我们进入临界区上锁后,如果发现条件不满足,那我们先解锁,然后在该条件变量下进行等待不就行了。

//错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false){
    pthread_mutex_unlock(&mutex);
    //解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
    pthread_cond_wait(&cond);
    pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);

但这是不可行的,因为解锁和等待不是原子操作,调用解锁之后,在调用pthread_cond_wait函数之前,如果已经有其他线程获取到互斥量,发现此时条件满足,于是发送了信号,那么此时pthread_cond_wait函数将错过这个信号,最终可能会导致线程永远不会被唤醒,因此解锁和等待必须是一个原子操作。

而实际进入

pthread_cond_wait

函数后,会先判断条件变量是否等于0,若等于0则说明不满足,此时会先将对应的互斥锁解锁,直到

pthread_cond_wait

函数返回时再将条件变量改为1,并将对应的互斥锁加锁。

条件变量使用规范

等待条件变量的代码

pthread_mutex_lock(&mutex);
while (条件为假)
    pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_unlock(&mutex);

唤醒等待线程的代码

pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

可重入VS线程安全

概念

  • 线程安全: 多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现线程安全问题。
  • 重入: 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则是不可重入函数。

注意: 线程安全讨论的是线程执行代码时是否安全,重入讨论的是函数被重入进入。

常见的线程不安全的情况

  • 不保护共享变量的函数。
  • 函数状态随着被调用,状态发生变化的函数。
  • 返回指向静态变量指针的函数。
  • 调用线程不安全函数的函数。

常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
  • 类或者接口对于线程来说都是原子操作。
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性。

常见的不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
  • 调用了标准I/O库函数,标准I/O可以的很多实现都是以不可重入的方式使用全局数据结构。
  • 可重入函数体内使用了静态的数据结构。

常见的可重入的情况

可重入与线程安全联系

可重入与线程安全区别

  • 不使用全局变量或静态变量。
  • 不使用malloc或者new开辟出的空间。
  • 不调用不可重入函数。
  • 不返回静态或全局数据,所有数据都由函数的调用者提供。
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
  • 函数是可重入的,那就是线程安全的。
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
  • 可重入函数是线程安全函数的一种。
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数的锁还未释放则会产生死锁,因此是不可重入的。

常见锁概念

死锁

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

单执行流可能产生死锁吗?

单执行流也有可能产生死锁,如果某一执行流连续申请了两次锁,那么此时该执行流就会被挂起。因为该执行流第一次申请锁的时候是申请成功的,但第二次申请锁时因为该锁已经被申请过了,于是申请失败导致被挂起直到该锁被释放时才会被唤醒,但是这个锁本来就在自己手上,自己现在处于被挂起的状态根本没有机会释放锁,所以该执行流将永远不会被唤醒,此时该执行流也就处于一种死锁的状态。

例如,在下面的代码中我们让主线程创建的新线程连续申请了两次锁。

#include <stdio.h>
#include <pthread.h>

pthread_mutex_t mutex;
void* Routine(void* arg)
{
    pthread_mutex_lock(&mutex);
    pthread_mutex_lock(&mutex);
    
    pthread_exit((void*)0);
}
int main()
{
    pthread_t tid;
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&tid, NULL, Routine, NULL);
    
    pthread_join(tid, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}

运行代码,此时该程序实际就处于一种被挂起的状态。用

ps

命令查看该进程时可以看到,**该进程当前的状态是

Sl+

**,其中的

l

实际上就是lock的意思,表示该进程当前处于一种死锁的状态。

什么叫做阻塞?

进程运行时是被CPU调度的,换句话说进程在调度时是需要用到CPU资源的,每个CPU都有一个运行等待队列(runqueue),CPU在运行时就是从该队列中获取进程进行调度的。

在运行等待队列中的进程本质上就是在等待CPU资源,实际上不止是等待CPU资源如此,等待其他资源也是如此,比如锁的资源、磁盘的资源、网卡的资源等等,它们都有各自对应的资源等待队列。

例如,当某一个进程在被CPU调度时,该进程需要用到锁的资源,但是此时锁的资源正在被其他进程使用:

  • 那么此时该进程的状态就会由R状态变为某种阻塞状态,比如S状态。并且该进程会被移出运行等待队列,被链接到等待锁的资源的资源等待队列,而CPU则继续调度运行等待队列中的下一个进程。
  • 此后若还有进程需要用到这一个锁的资源,那么这些进程也都会被移出运行等待队列,依次链接到这个锁的资源等待队列当中。
  • 直到使用锁的进程已经使用完毕,也就是锁的资源已经就绪,此时就会从锁的资源等待队列中唤醒一个进程,将该进程的状态由S状态改为R状态,并将其重新链接到运行等待队列,等到CPU再次调度该进程时,该进程就可以使用到锁的资源了。

总结一下:

  • 站在操作系统的角度,进程等待某种资源,就是将当前进程的task_struct放入对应的等待队列,这种情况可以称之为当前进程被挂起等待了。
  • 站在用户角度,当进程等待某种资源时,用户看到的就是自己的进程卡住不动了,我们一般称之为应用阻塞了。
  • 这里所说的资源可以是硬件资源也可以是软件资源,锁本质就是一种软件资源,当我们申请锁时,锁当前可能并没有就绪,可能正在被其他线程所占用,此时当其他线程再来申请锁时,就会被放到这个锁的资源等待队列当中。

死锁的四个必要条件

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

注意: 这是死锁的四个必要条件,也就是说只有同时满足了这四个条件才可能产生死锁。

避免死锁

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

除此之外,还有一些避免死锁的算法,比如死锁检测算法和银行家算法。

标签: 开发语言 c++ linux

本文转载自: https://blog.csdn.net/xwy13886467077/article/details/143776079
版权归原作者 闪电无敌雷霆霹雳爆炸猿 所有, 如有侵权,请联系我们删除。

“Linux线程安全”的评论:

还没有评论