当前位置: 首页 > article >正文

Linux线程_线程互斥_线程同步

一.线程互斥

1.进程线程间的互斥相关概念


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

有多个线程对临界资源num=10000进行--操作 直到num==0。如果不进行保护,num为什么会<0?

1.num--操作并不是原子性的,实际上分为三部 1.把数据从内存存入CPU的寄存器 2.数据-- 3.再把寄存器的数据写回内存

2.if(num>0) 也不是原子性的,分为读取和比较操作。如果刚读取完num==1,条件符合,线程1进入,还没进行--就被切换,线程2读取 条件仍符合,线程2进入。这样就可能会放入多个线程进入。

3.让线程尽可能的切换

线程什么时候会被切换?
1.时间片用完
2.高优先级线程抢占当前线程

3.当前线程进入阻塞状态(等待 I/O 或资源)

sleep会让当前线程进入等待队列,并切换其它线程进行调度。等到sleep结束,从内核态转为用户态,会检测时间片是否结束,结束进行切换其它线程。

因此根本原因就在于if(num>0)不是原子性的,让多个线程进入判断内部,导致num<0

2.互斥锁函数

解决上面的问题,就要完成在判断内部也就是临界区内只有一个线程执行

对临界区进行加锁就可以保证临界区内只有一个线程执行。

下面我们看一下锁的用法

1.PTHREAD_MUTEX_INITIALIZER宏初始化

有两种初始化锁的方法,这是用宏对锁静态初始化。

pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER

定义的时候就完成初始化,通常用于声明全局或静态的互斥锁。

初始化好锁后,怎么用呢?

1.进入临界区前 加锁 

pthread_mutex_lock()
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);

mutex指向要进行加锁操作的锁  成功返回0 失败返回错误码

2.出临界区后 解锁

pthread_mutex_unlock()
int pthread_mutex_unlock(pthread_mutex_t *mutex);

mutex指向要进行解锁操作的锁  成功返回0 失败返回错误码

2.pthread_mutex_init()函数初始化 

 先定义锁,再用函数初始化。适用于局部变量、需要在运行时初始化的互斥锁。

可以传递锁的属性。需要在使用完后调用 pthread_mutex_destroy 销毁。

#include <pthread.h>

pthread_mutex_t mutex;  // 声明但不初始化

void *thread_func(void *arg) {
    pthread_mutex_lock(&mutex);  // 加锁
    // 访问共享资源
    pthread_mutex_unlock(&mutex);  // 解锁
}

int main() {
    pthread_t thread1, thread2;
    
    // 动态初始化互斥锁
    pthread_mutex_init(&mutex, NULL);  // 初始化互斥锁
    
    pthread_create(&thread1, NULL, thread_func, NULL);
    pthread_create(&thread2, NULL, thread_func, NULL);
    
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    
    pthread_mutex_destroy(&mutex);  // 销毁互斥锁
    
    return 0;
}
 pthread_mutex_init()
 
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

mutex 指向要初始化的锁 attr锁的属性

pthread_mutex_destroy()
 
int pthread_mutex_destroy(pthread_mutex_t *mutex);

mutex指向要销毁的锁

每次进出临界区的时候都要进行加锁 解锁操作,有什么办法可以自动对临界区进行加锁 解锁?

#pragma once
#include <iostream>
#include <pthread.h>

namespace LockModule
{
    class Mutex
    {
    public:
        Mutex(const Mutex&) = delete;
        const Mutex& operator = (const Mutex&) = delete;
        Mutex()
        {
            int n = ::pthread_mutex_init(&_lock, nullptr);
            (void)n;
        }
        ~Mutex()
        {
            int n = ::pthread_mutex_destroy(&_lock);
            (void)n;
        }
        void Lock()
        {
            int n = ::pthread_mutex_lock(&_lock);
            (void)n;
        }
        void Unlock()
        {
            int n = ::pthread_mutex_unlock(&_lock);
            (void)n;
        }

    private:
        pthread_mutex_t _lock;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex &mtx):_mtx(mtx)
        {
            _mtx.Lock();
        }
        ~LockGuard()
        {
            _mtx.Unlock();
        }
    private:
        Mutex &_mtx;
    };
}

定义一个Mutex类,它的构造函数就是初始化成员变量_block锁,析构函数是销毁锁。有解锁 加锁的函数。再定义一个以Mutex类对象为成员变量的LockGuard类,构造函数进行加锁操作,析构函数进行解锁操作。这样创建一个LockGuard对象就完成了加锁,销毁时就完成了解锁操作。

我们把临界区单独放在一个作用域里面,在临界区的作用域里面一开始创建LockGuard对象自动调用构造函数完成加锁,等到出作用域自动调用析构函数,完成解锁。

3.互斥量实现原理

pthread_mutex_lock()到底怎么完成只让一个线程进入临界区,实现互斥操作的呢?

为了实现互斥锁操作,大多数体系结构都提供了 swap 或 exchange 指令,该指令 的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使 是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另 一个处理器的交换指令只能等待总线周期。 现在我们把 lock 和 unlock 的伪代码改一下。

在内存中互斥锁mutex可以理解成一个计数器,默认为1,代表没有线程在临界区,可以进入。为0代表有线程在临界区不能进入。

进入lock函数:

1.move $0 %al 把0值写入寄存器

2.xchgb 把寄存器数据0和内存mutex数据交换(原子性)

3.如果寄存器内数据>0 代表没有线程在临界区,可以进入。

==0 有线程在临界区,进入等待队列。等待结束后再重新执行lock函数 直到>0返回

unlock函数:

把1写入mutex中 (原子性),退出临界区,表示允许线程进入临界区。

二.线程同步

1.同步概念

互斥可以保证只有一个线程进入临界区访问临界资源,但多个线程竞争同一份临界资源,如果有一个线程总是能访问到,而其它线程一直在等待,这就造成了饥饿问题。怎么才能解决线程间资源的竞争呢?

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

同步的机制有 互斥锁(确保每次只有一个线程能访问临界区,防止并发访问同一资源)

条件变量 (让线程在某个条件满足之前挂起,直到其他线程通知它继续执行)

信号量 (用于控制访问共享资源的线程数量)

条件变量可以协调线程的执行顺序。

2.条件变量函数

初始化

1.PTHREAD_COND_INITIALIZER宏

一般用于初始化全局条件变量

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
2.int pthread_cond_init()
 
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

cond:指向要初始化的条件变量的指针。
attr:用于设置条件变量的属性,一般传入 NULL,表示使用默认属性。
返回值:成功时返回 0,失败时返回错误码。

pthread_cond_destroy()销毁
 

int pthread_cond_destroy(pthread_cond_t *cond);

pthread_cond_wait() 等待条件变量
 

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

cond:条件变量。
mutex:互斥锁,在调用 pthread_cond_wait 时必须持有该锁。调用 wait 会解锁,并使当前线程阻塞,直到条件变量被通知。
返回值:成功时返回 0,失败时返回错误码。
注意:pthread_cond_wait 会解锁并使当前线程阻塞,直到条件变量收到通知,线程被唤醒后,重新加锁。

pthread_cond_signal() 通知一个线程
 

int pthread_cond_signal(pthread_cond_t *cond);

cond:条件变量,通知一个等待该条件变量的线程。
返回值:成功时返回 0,失败时返回错误码。

pthread_cond_broadcast() 通知所有线程
 

int pthread_cond_broadcast(pthread_cond_t *cond);

cond:条件变量,通知所有等待该条件变量的线程。
返回值:成功时返回 0,失败时返回错误码。

3.生产消费者模型

1.什么是生产消费者模型

互斥锁和条件变量怎么搭配使用呢?
先讲一个场景,生产消费者模型。

有多个工厂向超市放资源,超市存入资源且大小有限,消费者从超市拿资源。

可以理解为有多个线程向临界区存入资源,有多个线程从临界区取资源。在此过程中要保证临界区的安全,还有保证高效。

在此场景中有三种关系:

1.生产者和生产者 两个生产者可以同时进入超市吗?不能,如果超市刚好差一个资源就满,那么就不能保证资源安全。

所以生产者和生产者关系为:互斥

2.消费者和消费者 两个消费者可以同时进入超市吗?不能,超市有只有一个资源的情况

所以消费者和消费者关系为:互斥

3.消费者和生产者 一个消费者和一个生产者可以同时进入吗?不能,消费者在取资源时怎么知道生产者有没有放资源,消费者先取发现没有,回去说里面没有资源,生产者再放,回去说里面有资源,造成二义性。

所以消费者和生产者关系为:互斥

消费者发现超市没有资源,就回去,等一段时间再访问有没有资源,不停循环访问。这样虽然合理,但不高效。如果能什么时候有资源了,就通知一声消费者再让它过来就可以了,

那么谁知道什么时候有资源呢?生产者,因为相互互斥,等生产者出来的时候一定有资源,让生产者通知消费者取资源就可以了。反过来,消费者也知道什么时候资源没满,消费者出来的时候,再通知生产者存入资源就可以了。

所以消费者和生产者关系为:互斥+同步

下面我们用代码模拟上述场景,多个线程分别当作生产者 消费者,用阻塞队列当作有资源上线的超市。(当队列为空时,消费者线程会被阻塞,直到队列中有元素可以消费当队列满时,生产者线程会被阻塞,直到队列中有空位可以插入新的元素。阻塞队列常用于多线程环境中的生产者-消费者问题等场景。)

2.代码模拟

#pragma once

#include<iostream>
#include<cstdio>
#include<queue>
#include "Mutex.hpp"
#include "Cond.hpp"

using namespace LockModule;
using namespace CondModule;


namespace BlockQueueModule
{
    static const int gcap = 10;
    template<typename T>
    class BlockQueue
    {
    private:
        bool IsFull(){return _cap==_q.size();}
        bool IsEmpty(){return _q.empty();}
    public:
        BlockQueue(int cap=gcap):_cap(cap)
        {
            // pthread_mutex_init(&_mutex,nullptr);
            // pthread_cond_init(&_p_cond,nullptr);
            // pthread_cond_init(&_c_cond,nullptr);
        }
        void Pop(int*data)
        {
            //pthread_mutex_lock(&_mutex);
            LockGuard lockguard(_mutex);
            //1.队列不能为空
            while(IsEmpty())
            {
                _c_nums++;
                //pthread_cond_wait(&_c_cond,&_mutex);
                _c_cond.Wait(_mutex);
                _c_nums--;
            }
            *data=_q.front();
            _q.pop();
            //if(_p_nums) pthread_cond_signal(&_p_cond);
            if(_p_nums) _c_cond.Notify();
            //pthread_mutex_unlock(&_mutex);
        }
        void Equeue(const int&data) //生产者
        {
            //pthread_mutex_lock(&_mutex);
            LockGuard lockguard(_mutex);//初始化自动上锁 出作用域解锁
            //1.放入数据前队列不能为满
            while(IsFull())
            {
                //2.为满就让该线程释放锁 进入等待队列
                _p_nums++;// 等待生产者线程+1
                //pthread_cond_wait(&_p_cond,&_mutex);//wait的时候 有锁的话 会导致消费线程申请不到锁 导致一直在等待
                _p_cond.Wait(_mutex);
                _p_nums--;
                //3.等待结束唤醒线程 并申请锁(因为在临界区内,pthread_cond_wait会自动申请锁)
            }
            //4.到此时肯定未满 生产者存入数据
            _q.push(data);
            //5.此时肯定有数据

            //if(_c_nums) pthread_cond_signal(&_c_cond); //有等待的消费者线程就唤醒一个
            if(_c_nums) _p_cond.Notify();
            //6.唤醒线程要在解锁前还是解锁后?
            //signal通知线程 线程被唤醒后还要申请到锁才能继续执行
            //(1)在解锁前唤醒它 它申请不到锁 就会阻塞在申请锁的队列中 直到申请到锁
            //(2)如果被其它进程申请到了锁 就继续阻塞在申请锁的队列中 
            //7.如果生产者只生成了1个资源,但broadcast()唤醒了等待队列的所有线程会怎么样?
            //会发生伪唤醒。第一个申请到锁的线程获取了资源。现在没有资源了,可是第二个申请到锁的线程还是会在临界区内继续执行 
            //解决方法:if()改为while()判断 在线程被唤醒后,重新检查条件是否满足,并在不满足时重新进入等待状态。
            //(即重新看是否满足while的判断,满足就重新进入等待队列,不满足退出while循环)
            //pthread_mutex_unlock(&_mutex);
        }
        ~BlockQueue()
        {
            // pthread_mutex_destroy(&_mutex);
            // pthread_cond_destroy(&_p_cond);
            // pthread_cond_destroy(&_c_cond);
        }
    private:
        std::queue<T> _q;//队列
        int _cap;        //队列数据上限
        Mutex _mutex; //互斥
        Cond _p_cond; //生产者条件变量
        Cond _c_cond; //消费者条件变量
        int _c_nums=0;  //等待的消费者线程
        int _p_nums=0;  //等待的生产者线程
    };
}

















对条件变量的封装

#pragma once

#include "Mutex.hpp"

using namespace LockModule;

namespace CondModule
{
    class Cond
    {
    public:
        Cond()
        {
            int n=pthread_cond_init(&_cond,nullptr);
            (void)n;
        }
        void Wait(Mutex&mutex)
        {
            int n=pthread_cond_wait(&_cond,mutex.LockPtr());
            (void)n;
        }
        void Notify()
        {
            int n=pthread_cond_signal(&_cond);
            (void)n;
        }
        void NotifyAll()
        {
            int n=pthread_cond_broadcast(&_cond);
            (void)n;
        }
        ~Cond()
        {
            int n=pthread_cond_destroy(&_cond);
            (void)n;
        }
    private:
        pthread_cond_t _cond;
    };
}

对锁的封装

#pragma once
#include <iostream>
#include <pthread.h>

namespace LockModule
{
    class Mutex
    {
    public:
        Mutex(const Mutex&) = delete;
        const Mutex& operator = (const Mutex&) = delete;
        Mutex()
        {
            int n = ::pthread_mutex_init(&_lock, nullptr);
            (void)n;
        }
        ~Mutex()
        {
            int n = ::pthread_mutex_destroy(&_lock);
            (void)n;
        }
        void Lock()
        {
            int n = ::pthread_mutex_lock(&_lock);
            (void)n;
        }
        pthread_mutex_t *LockPtr()
        {
            return &_lock;
        }
        void Unlock()
        {
            int n = ::pthread_mutex_unlock(&_lock);
            (void)n;
        }

    private:
        pthread_mutex_t _lock;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex &mtx):_mtx(mtx)
        {
            _mtx.Lock();
        }
        ~LockGuard()
        {
            _mtx.Unlock();
        }
    private:
        Mutex &_mtx;
    };
}

上层接口

#include"BlcokQueue.hpp"
#include<pthread.h>
#include<unistd.h>


using namespace BlockQueueModule;

void* Consumer(void*args)
{
    BlockQueue<int>*bq=static_cast<BlockQueue<int>*>(args);
    int data;
    while (true)
    {
        sleep(2);
        //1.从队列中获取数据
        bq->Pop(&data);
        //2.处理数据
        printf("消费者获取数据:%d\n",data);
    }
    
}

void* Productor(void*args)
{
    BlockQueue<int>*bq=static_cast<BlockQueue<int>*>(args);
    //1.从外部获取数据
    int data=10;
    while(true)
    {
        //2.把数据输入队列中
        bq->Equeue(data);
        printf("生产者存入数据:%d\n",data);
        data++;
    }
}
int main()
{
    BlockQueue<int>* bq=new(BlockQueue<int>);
    pthread_t c,p;
    pthread_create(&c,nullptr,Consumer,(void*)bq);
    pthread_create(&p,nullptr,Productor,(void*)bq);


    pthread_join(p,nullptr);
    pthread_join(c,nullptr);
    delete bq;

}

3.生产消费者模型高效在哪里?

生产消费者模型,三种关系生产者和生产者 消费者和消费者 生产者和消费者都是互斥的,在临界区内都是串行的,有一个线程在内,其它线程都不能进来,怎么就高效了呢?

想一想,当其中一个线程在临界区内,其它线程呢?

1.生产者在从外部获取数据,不需要等待消费者处理完数据。消费者在处理数据,不需要等待生产者生成数据。
通过这种方式,生产者和消费者可以并行运行,避免了阻塞和等待,从而提高了资源的利用率。

2.临界区可以当作缓冲区,调节生产与消费的速度。

如果生产者的生产速度快于消费者的消费速度,缓冲区会存储数据,生产者可以继续生产而不会被阻塞。
如果消费者的消费速度快于生产者的生产速度,缓冲区中的数据会被快速消费,生产者有更多的时间来生产新数据。


http://www.kler.cn/a/407339.html

相关文章:

  • DevOps-Jenkins-新手入门级
  • 怎么只提取视频中的声音?从视频中提取纯音频技巧
  • OpenCV双目立体视觉重建
  • 任务通知的本质(任务通知车辆运行) 软件定时器的本质(增加游戏音效)
  • 操作系统——揭开盖子
  • 提升软件测试报告的质量:Allure2中添加用例失败截图、日志、HTML块和视频的方法
  • QT文件基本操作
  • BERT的中文问答系统35
  • 猎板科技:PCB 特殊定制领域的卓越引领者
  • 汽车软件开发中的ASPICE合规挑战与Jama Connect解决方案
  • 解决整合Django与Jinja2兼容性的问题
  • django+boostrap实现注册
  • MD5算法的学习
  • 08 —— Webpack打包图片
  • 谷粒商城-消息队列Rabbitmq
  • 从熟练Python到入门学习C++(record 6)
  • AMD64(Advanced Micro Devices) 超微半导体 (x86-64)
  • leecode45.跳跃游戏||
  • rembg AI扣图
  • php:使用Ratchet类实现分布式websocket服务
  • 第三百二十八节 Java网络教程 - Java网络TCP客户端套接字
  • PLC的指令全集1+TIA PORTAL仿真(西门子S7 1200)
  • 浮点数的表示—IEEE754标准
  • c#:winform引入bartender
  • 【大数据技术基础】 课程 第5章 HBase的安装和基础编程 大数据基础编程、实验和案例教程(第2版)
  • Windows之使用putty软件以ssh的方式连接Linux中文显示乱码