【Linux--多线程同步与互斥】

目录

一、线程互斥

1.1相关概念介绍

  • 临界资源: 多线程执行流共享的资源叫做临界资源
  • 临界区: 每个线程内部访问临界资源的代码,被称为临界区
  • 互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区访问临界资源,通常对临界资源起保护作用
  • 原子性: 不会被任何调度机制打断的操作,该操作只有两态:要么完成,要么未完成
    为什么要有线程互斥?下面模拟下4人分苹果的代码。
    代码:
cpp 复制代码
#include<iostream>
using namespace std;
#include<pthread.h>
#include<string>
#include<vector>
#include<unistd.h>
const int NUM=4;
class ThreadDate
{
public: 
    ThreadDate(string name)
    {
        _name=name;
    }
    string _name;
};
int Apples=100;
void* GetApple(void* args)
{
    ThreadDate* td=static_cast<ThreadDate*>(args);
    while(1)
    {
        if(Apples>0)
        {
            sleep(1);
            cout<<td->_name<<"get a apple,apple number"<<--Apples<<endl;
        }
        else break;
    }
    return nullptr;
}
int main()
{

    vector<pthread_t> tids;
    vector<ThreadDate*> tds;

    for(int i=0;i<NUM;i++)
    {
        string name="thread"+to_string(i);
        ThreadDate* td=new ThreadDate(name);
        tds.push_back(td);
        pthread_t tid;
        pthread_create(&tid,nullptr,GetApple,tds[i]);
        tids.push_back(tid);
    }

    for(int i=0;i<NUM;i++)
    {
        pthread_join(tids[i],nullptr);
        delete tds[i];
    }
    return 0;
}

现象:

产生该现象的原因:

1.2互斥量mutex

若线程使用的数据是局部变量,变量的地址空间在线程栈空间内,变量归属单个线程,其他线程无法获得这种变量;但有些变量需要在线程间共享(共享变量),可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量就会带来一些问题

要解决上述分苹果的问题,需要做到三点:

代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

若多个线程同时要求执行临界区的代码,并且此时临界区没有线程在执行,那么只能允许一个线程进入该临界区

若线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

这时就需要一把锁,Linux中提供的这把锁被称为互斥量

1.3互斥量接口

1.3.1初始化互斥量

cpp 复制代码
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数:

  • mutex:需要初始化的互斥量的地址
  • attr:初始化互斥量的属性,一般设置为nullptr即可
    返回值:
  • 互斥量初始化成功返回0,失败返回错误码
    使用pthread_mutex_init()函数初始化互斥量的方式被称为动态分配,还可以使用静态分配进行初始化,即下面这种方式:
cpp 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

1.3.2销毁互斥量

cpp 复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数mutex:需要销毁的互斥量的地址

返回值:互斥量销毁成功返回0,失败返回错误码

注意:

  • 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

1.3.3互斥量加锁

cpp 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);

参数mutex:需要加锁的互斥量的地址

返回值:互斥量加锁成功返回0,失败返回错误码

注意:

  • 互斥量处于未锁状态时,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,若其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么线程会在pthread_mutex_lock()函数内部阻塞至互斥量解锁

1.3.4互斥量解锁

cpp 复制代码
int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数mutex:需要解锁的互斥量的地址

返回值:互斥量解锁成功返回0,失败返回错误码

1.3.5使用互斥量解决上面分苹果问题

cpp 复制代码
#include<iostream>
using namespace std;
#include<pthread.h>
#include<string>
#include<vector>
#include<unistd.h>
const int NUM=4;
class ThreadDate
{
public: 
    ThreadDate(string name,pthread_mutex_t* lock)
    {
        _name=name;
        _lock=lock;
    }
public:
    string _name;
    pthread_mutex_t* _lock;

};
int Apples=100;
void* GetApple(void* args)
{
    ThreadDate* td=static_cast<ThreadDate*>(args);
    while(1)
    {
        pthread_mutex_lock(td->_lock);
        if(Apples>0)
        {
            //sleep(1);
            cout<<td->_name<<"get a apple,apple number"<<Apples--<<endl;
            pthread_mutex_unlock(td->_lock);
        }
        else 
        {
            pthread_mutex_unlock(td->_lock);
            break;
        }
        
    }
    return nullptr;
}
int main()
{
    pthread_mutex_t lock;
    pthread_mutex_init(&lock,nullptr);
    vector<pthread_t> tids;
    vector<ThreadDate*> tds;

    for(int i=0;i<NUM;i++)
    {
        string name="thread"+to_string(i);
        ThreadDate* td=new ThreadDate(name,&lock);
        tds.push_back(td);
        pthread_t tid;
        pthread_create(&tid,nullptr,GetApple,tds[i]);
        tids.push_back(tid);
    }

    for(int i=0;i<NUM;i++)
    {
        pthread_join(tids[i],nullptr);
        delete tds[i];
    }
    pthread_mutex_destroy(&lock);
    return 0;
}

写这个代码的时候出现了一个乌龙。写到这里复盘以下,顺便提一嘴,多线程写代码时考虑的是要多一点。

1.4互斥原理

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

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

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

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

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

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

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用就是把寄存器和内存单元的数据相交换。由于只有一条指令,保证了原子性。
lock和unlock的伪代码:

二、可重入与线程安全

2.1相关概念

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

2.2常见线程不安全的情况

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

2.3常见不可重入的情况

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

2.4 可重入与线程安全的关系

  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的(可重入函数是线程安全函数的一种)
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 若一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
  • 若对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数的锁还未释放则会产生死锁,因此是不可重入的

三、死锁

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

多个锁锁死

线程A申请锁资源的顺序为:锁1、锁2;线程B申请锁资源的顺序为:锁2、锁1

当线程A申请到锁1准备申请锁2时,线程B已申请到锁2准备申请锁1,这时两个线程都会因为申请锁失败而陷入阻塞,并且无法释放锁,进入死锁状态
产生死锁的条件:

  • 互斥条件: 一个资源每次只能被一个执行流使用
  • 请求与保持条件: 一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件: 一个执行流已获得的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系
    避免死锁
  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

四、线程同步

4.1同步概念与竞态条件

同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这就叫做同步

竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件

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

4.2条件变量

4.2.1概念

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

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

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

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

4.2.2接口

4.2.2.1初始化条件变量

cpp 复制代码
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

参数:

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

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

使用pthread_cond_init()函数初始化条件的方式被称为动态分配,还可以使用静态分配进行初始化,即下面这种方式:

cpp 复制代码
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

4.2.2.2销毁条件变量

cpp 复制代码
int pthread_cond_destroy(pthread_cond_t *cond);

参数cond:需要销毁的条件变量的地址

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

注意:使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要销毁

4.2.2.2等待条件变量满足

cpp 复制代码
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

参数:

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

返回值:函数调用成功返回0,失败返回错误码

4.2.2.3唤醒等待

cpp 复制代码
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
  • pthread_cond_signal()函数用于唤醒该条件变量等待队列中首个线程
  • pthread_cond_broadcast()函数用于唤醒该条件变量等待队列中的全部线程

参数cond:唤醒在cond条件变量下等待的线程

返回值:函数调用成功返回0,失败返回错误码

4.2.2.5改进分苹果

cpp 复制代码
#include<iostream>
using namespace std;
#include<pthread.h>
#include<string>
#include<vector>
#include<unistd.h>
const int NUM=4;

pthread_cond_t cond=PTHREAD_COND_INITIALIZER;


class ThreadDate
{
public: 
    ThreadDate(string name,pthread_mutex_t* lock)
    {
        _name=name;
        _lock=lock;
    }
public:
    string _name;
    pthread_mutex_t* _lock;

};
int Apples=10;
int flag=NUM;
void* GetApple(void* args)
{
    ThreadDate* td=static_cast<ThreadDate*>(args);
    while(1)
    {
        pthread_mutex_lock(td->_lock);
        pthread_cond_wait(&cond,td->_lock);//线程在等待队列的时候会自动释放锁
        if(Apples>0)
        {
            cout<<td->_name<<"get a apple,apple number"<<Apples--<<endl;
            pthread_mutex_unlock(td->_lock);
        }
        else 
        {
            pthread_mutex_unlock(td->_lock);
            break;
        }
        
    }
    cout<<td->_name<<" "<<"quit!"<<endl;
    pthread_mutex_lock(td->_lock);
    pthread_cond_wait(&cond,td->_lock);
    flag--;
    pthread_mutex_unlock(td->_lock);
    return nullptr;
}
int main()
{
    pthread_mutex_t lock;
    pthread_mutex_init(&lock,nullptr);
    vector<pthread_t> tids;
    vector<ThreadDate*> tds;

    for(int i=0;i<NUM;i++)
    {
        string name="thread"+to_string(i);
        ThreadDate* td=new ThreadDate(name,&lock);
        tds.push_back(td);
        pthread_t tid;
        pthread_create(&tid,nullptr,GetApple,tds[i]);
        tids.push_back(tid);
    }
    sleep(3);
    while(flag)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }

    for(int i=0;i<NUM;i++)
    {
        pthread_join(tids[i],nullptr);
        delete tds[i];
    }
    pthread_mutex_destroy(&lock);
    pthread_cond_destroy(&cond);
    return 0;
}
相关推荐
且从容.2 小时前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道3 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
且从容.5 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落7 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨7 小时前
WSL2的安装与使用
linux·wsl
微小的鱼7 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng8 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮8 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
Linux-lucky20 小时前
45-Linux学习之旅之防火墙与WEB加固
linux·前端·网络·学习·ubuntu
禁默20 小时前
启元 Q1 二次开发实践:Ubuntu 部署与仿真验证,从 PrimeConsole 到 ROS 2 Humble + RViz
linux·运维·ubuntu