多线程与锁


1、死锁

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

1.1 产生死锁的四个必要条件

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

1.2 避免死锁

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

问题:
一个线程一把锁有没有可能形成死锁呢?
答案是当然有可能,例如对一把锁进行多次加锁,而不解锁,就会形成死锁
代码演示:
在这里插入图片描述

在这里插入图片描述

2、Linux线程同步

2.1 同步概念与竞态条件

同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步(Linux中的管道就自带同步机制)。
竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。

同步存在是为了多线程协同高效的进行合作

2.2 条件变量

  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了
  • 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量

2.3 操作条件变量的相关接口

pthread原生线程库给用户提供了能够操作条件变量的相关接口,例如初始化条件变量、销毁条件变量等。

2.3.1 初始化条件变量

在这里插入图片描述
参数:

  • cond:要初始化的条件变量
  • attr:NULL

返回值:

  • 成功返回0,失败返回一个错误码

如果定义一个全局或者静态的条件变量,也可以直接使用宏PTHREAD_COND_INITIALIZER;来进行初始化,例如:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER

2.3.2 销毁条件变量

在这里插入图片描述
参数:

  • cond:要销毁的条件变量

返回值:

  • 成功返回0,失败返回一个错误码

2.3.3 等待条件满足

在这里插入图片描述
参数:

  • cond:要在这个条件变量上等待
  • mutex:互斥量

返回值:

  • 成功返回0,失败返回一个错误码

2.3.4 唤醒等待

在这里插入图片描述
pthread_cond_brpadcast和pthread_cond_signal的参数和返回值都是一样的
参数:

  • cond:条件变量

返回值:

  • 成功返回0,失败返回一个错误码

两者的区别:

  • pthread_cond_broadcast是唤醒在cond条件变量下的全部进程
  • pthread_cond_signal是唤醒在cond条件变量下的一个进程

2.4 代码演示

#include <string>
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

//全局的条件变量和互斥锁能被所有线程看到
pthread_mutex_t mtx;
pthread_cond_t cond;
// ctrl thread 控制work线程,让它定期运行
void *ctrl(void *args)
{
    string name = (char *)args;
    while (true)
    {
        //唤醒在条件变量下的一个线程
        pthread_cond_signal(&cond);
        sleep(1);
    }
}

void *work(void *args)
{
    int number = *(int *)args;
    delete (int *)args;
    while (true)
    {
        //在cond条件变量下进行等待
        pthread_cond_wait(&cond, &mtx);
        cout << "worker:" << number << " is working..." << endl;
    }
}
int main()
{
#define NUM 5
    //初始化互斥锁和条件变量
    pthread_mutex_init(&mtx, nullptr);
    pthread_cond_init(&cond, nullptr);

    //创建6个线程,master线程用来控制5个worker线程
    pthread_t master;
    pthread_t worker[5];
    pthread_create(&master, nullptr, ctrl, (void *)"boss");
    for (int i = 0; i < NUM; ++i)
    {
        int *number = new int(i);
        pthread_create(worker + i, nullptr, work, (void *)number);
    }

    for (int i = 0; i < NUM; ++i)
    {
        pthread_join(worker[i], nullptr);
    }
    pthread_join(master, nullptr);

    //释放互斥锁和条件变量
    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&cond);
    return 0;
}

运行代码:
在这里插入图片描述
如果将 pthread_cond_signal(&cond)改为pthread_cond_broadcast(&cond),再运行代码:
在这里插入图片描述
通过上面的测试,我们发现好像worker的执行顺序好像是有序的,这是为什么呢?

其实可以把worker的等待过程想象成队列,当一个worker执行完毕后,又要进行等待时,此时就需要在队尾进行等待,所以在用户看来就是一个排队的过程

为什么pthread_cond_wait()需要互斥量呢?
原因:

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程
  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据
  • 简单来说,pthread_cond_wait()中还要带上锁是因为,如果该线程在等待时,此时应该将临界区进行解锁,让另外的线程在该线程等待时,可以进入到临界区,要不然它在等待时还一直加锁状态,就没意义。当别的进程给该进程发信号,唤醒它,这个时候从新加锁,访问临界区,保证其原子性

3、生产者消费者模型

为何要使用生产者消费者模型 ?

  • 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接放在阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的

什么时候该生产,什么时候该消费?

  • 生产者不知道什么时候该生产,但是它知道什么时候该消费。消费者不知道什么时候该消费,但它知道什么时候该生产

为什么这么说呢?

因为生产者生产数据,放进阻塞队列时,说明已经有数据可以被消费了,此时就应该告诉消费者你该消费了。当阻塞队列被放满时,生产者就无法生产数据,但是什么时候又要开始生产呢?生产者并不知道,因为它不知道消费者什么时候消费。
当消费者消费数据时,阻塞队列的数据就会减少,此时就能告诉生产者,数据被我消费了,你可以开始生产了。当阻塞队列没有数据时,消费者就无法消费数据,但是什么时候又可以开始消费呢?消费者并不知道,因为它不知道生产者什么时候生产。

生产者消费者模型优点:

  1. 解耦
  2. 支持并发
  3. 支持忙闲不均

3.1 图示详解

在这里插入图片描述

  • 基于BlockingQueue的生产者消费者模型,在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。
  • 其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞(消费者),直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞(生产者),直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

生产者消费者"321"原则----方便理解

  • 3->3种关系:生产者&消费者(互斥/同步),生产者&生产者(竞争&互斥),消费者&消费者(竞争&互斥)
  • 2->2种角色:生产者(n个),消费者(n个),两种执行流
  • 3->一个交易场所(一段缓冲区)

3.2 模拟阻塞队列的生产消费模型

//BlockQueue.hpp

#pragma once
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <queue>
#include <ctime>
#include <cstdlib>
using namespace std;

namespace ns_blockqueue
{
    const int default_cap = 5;
    template <class T>
    class BlockQueue
    {
    private:
        queue<T> _bq;         //阻塞队列
        int _cap;             //队列元素上限
        pthread_mutex_t _mtx; //保护临界资源的锁
        // 1. 当队列满了,就不应该生产了(不要竞争锁了),而应该让消费者进行消费
        // 2. 当队列空了,就不应该消费了(不要竞争锁了),而应该让生产者进行生产
        pthread_cond_t _is_full;  // bq_满了,消费者应该在该条件变量下等待
        pthread_cond_t _is_empty; // bq_空了,生产者应该在该条件变量下等待

        bool IsFull()
        {
            return _bq.size() == _cap;
        }

        bool IsEmpty()
        {
            return _bq.size() == 0;
        }

        void LockQueue()
        {
            pthread_mutex_lock(&_mtx);
        }

        void UnLockQueue()
        {
            pthread_mutex_unlock(&_mtx);
        }

        void ProducterWait()
        {
            // pthread_cond_wait
            // 1. 调用的时候,会首先自动释放_mtx,然后在挂起自己
            // 2. 返回的时候,首先会自动竞争锁,获取到锁之后,才能返回
            pthread_cond_wait(&_is_empty, &_mtx);
        }

        void ConsumerWait()
        {
            pthread_cond_wait(&_is_full, &_mtx);
        }

        void WakeupConsumer()
        {
            pthread_cond_signal(&_is_full);
        }

        void WakeupProducter()
        {
            pthread_cond_signal(&_is_empty);
        }

    public:
        BlockQueue(int cap = default_cap)
            : _cap(cap)
        {
            pthread_mutex_init(&_mtx, nullptr);
            pthread_cond_init(&_is_full, nullptr);
            pthread_cond_init(&_is_empty, nullptr);
        }

        ~BlockQueue()
        {
            pthread_mutex_destroy(&_mtx);
            pthread_cond_destroy(&_is_full);
            pthread_cond_destroy(&_is_empty);
        }

        //生产函数,向队列里放数据
        void Push(const T &in) // in 为输入型参数
        {
            LockQueue();
            //临界区
            if (IsFull())
            {
                //队列满了,则无法生产,需要等待消费者消费
                //等待时,把线程挂起,但是现在该线程是持有锁的
                ProducterWait();
            }

            _bq.push(in);
            WakeupConsumer();
            UnLockQueue();
        }

        //消费函数,向队列里拿数据
        void Pop(T *out) // out 为输出型参数
        {
            //临界区
            LockQueue();
            if (IsEmpty())
            {
                //队列空了,则无法消费,需要等待生产者生产
                ConsumerWait();
            }
            *out = _bq.front();
            _bq.pop();
            WakeupProducter();
            UnLockQueue();
        }
    };
}
//CpTest.cpp

#include "BlockQueue.hpp"
using namespace ns_blockqueue;

void *consumer(void *args)
{
    BlockQueue<int> *_bq = (BlockQueue<int> *)args;
    while (true)
    {
        int data = 0;
        _bq->Pop(&data);
        cout << "消费者消费了一个数据: " << data << endl;
        sleep(1);
    }
}

void *producter(void *args)
{
    BlockQueue<int> *_bq = (BlockQueue<int> *)args;
    while (true)
    {
        //产生0到20的随机数
        int data = rand() % 20 + 1;
        cout << "生产者生产数据: " << data << endl;
        _bq->Push(data);
        //sleep(2);
    }
}

int main()
{
    srand((long long)time(nullptr));//种下随机数种子
    BlockQueue<int> *bq = new BlockQueue<int>();

    pthread_t c, p; // c为消费者, 跑为生产者
    pthread_create(&c, nullptr, consumer, (void *)bq);
    pthread_create(&p, nullptr, producter, (void *)bq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

使用sleep函数让消费者每隔1秒消费一个数据:

在这里插入图片描述
使用sleep函数让生产者每隔1秒生产一个数据,消费者每隔2秒消费一个数据:
在这里插入图片描述

生产者消费者模型的优点
生产者消费者模型,是实际开发中非常有用的一种多线程开发手段,尤其是在服务器开发的场景中。

假设,有两个服务器A和B,A作为入口服务器直接接收用户的网络请求,B作为应用服务器,来给A提供一些数据

在这里插入图片描述
如果不使用生产者消费者模型此时A和B的耦合性是比较强的!!
在开发A代码的时候就得充分了解到B提供的一些接口,开发B代码的时候也得充分了解到A是怎么调用的,一旦想把B换成C,A的代码就需要较大的改动,并且如果B挂了,也可能直接导致A也顺带挂了

优点1:使用生产者消费者模型,就可以降低这里的耦合

在这里插入图片描述
对于请求: A是生产者,B是消费者
对于响应: A是消费者,B是生产者

A只需要关注如何和队列交互,不需要认识B
B也只需要关注如何和队列交互,也不需要认识A
如果B挂了,对于A没啥影响。如果把B换成C,A也完全感知不到

优点2:能够对于请求进行"削峰填谷"

在这里插入图片描述

未使用生产者消费者模型的时候,如果请求量突然暴涨(可控)
A作为入口服务器,计算量很轻,请求暴涨,问题不大.
B作为应用服务器计算量可能很大,需要的系统资源也更多,如果请求更多了,需要的资源进一步增加,如果主机的硬件不够,可能程序就挂了

在这里插入图片描述
A请求暴涨会导致阻塞队列的请求暴涨
由于阻塞队列没啥计算量,就只是单纯的存个数据,就能抗住更大的压力
B这边仍然按照原来的速度来消费数据,不会因为A的暴涨而引起暴涨.
B就被保护的很好,就不会因为这种请求的波动而引起崩溃

“削峰”:这种峰值很多时候不是持续的,就一阵,过去了就又恢复了
“填谷”:B仍然是按照原有的频率来处理之前积压的数据

实际开发中使用到的"阻塞队列"并不是一个简单的数据结构了,是一个/一组专门的服务器程序。并且它提供的功能也不仅仅是阻塞队列的功能,还会在这基础之上提供更多的功能(对于数据持久化存储,支持多个数据通道,支持多节点容灾冗备份,支持管理面板,方便配置参数…)

4、POSIX信号量

4.1 POSIX概念

  • POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步
    -在这里插入图片描述

4.2 POSIX函数

4.2.1 初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value)

参数:

  • sem:需要初始化的信号量
  • pshared:0表示线程间共享,非零表示进程间共享
  • value:设置初始值

返回值:

-成功返回0,失败返回-1,同时错误码将被设置

4.2.2 销毁信号量

int sem_destroy(sem_t *sem)

参数:

  • sem:需要销毁的信号量

返回值:

-成功返回0,失败返回-1,同时错误码将被设置

4.2.3 等待信号量

int sem_wait(sem_t *sem);//P()操作

sem_wait表示需要申请信号量,如果申请不成功则会被挂起进行等待

参数:

  • sem:需要等待的信号量

返回值:

-成功返回0,失败返回-1,同时错误码将被设置

4.2.4 发布信号量

int sem_post(sem_t *sem)//V()操作

参数:

  • sem:需要发布的信号量

返回值:

-成功返回0,失败返回-1,同时错误码将被设置

4.3 基于环形队列的生产消费模型

  • 环形队列采用vector来模拟,用模运算来模拟环状特性
  • 环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态
  • 但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程

在这里插入图片描述

4.4 代码演示

//ring_queue.hpp
#pragma once
#include <vector>
#include <iostream>
#include <semaphore.h>

using namespace std;

namespace ns_ring_queue
{
    const int g_cap_default = 10;
    template <class T>
    class RingQueue
    {
    private:
        vector<T> _ring_queue;
        int _cap;

        //生产者关心空位置资源
        sem_t blank_sem;
        //消费者关心数据资源
        sem_t data_sem;

        //生产者生产数据的位置
        int p_step;
        //消费者消费数据的位置
        int c_step;

    public:
        RingQueue(int cap = g_cap_default)
            : _cap(cap), _ring_queue(cap), c_step(0), p_step(0)
        {
            sem_init(&blank_sem, 0, cap);
            sem_init(&data_sem, 0, 0);
        }

        ~RingQueue()
        {
            sem_destroy(&blank_sem);
            sem_destroy(&data_sem);
        }

        //生产
        void Push(const T &in)
        {
            //申请信号量
            sem_wait(&blank_sem); // P(空位置)
            //可以生产了,可是往哪个位置生产呢?
            _ring_queue[p_step] = in;
            sem_post(&data_sem); // V(数据)
            p_step++;
            p_step %= _cap;
        }

        //消费
        void Pop(T *out)
        {
            sem_wait(&data_sem); // P(数据)
            *out = _ring_queue[c_step];
            sem_post(&blank_sem); // V(空位置)
            c_step++;
            c_step %= _cap;
        }
    };
}



//ring_cp.cpp
#include "ring_queue.hpp"
#include <pthread.h>
#include <time.h>
#include <unistd.h>
using namespace ns_ring_queue;

void *consumer(void *args)
{
    RingQueue<int> *rq = (RingQueue<int> *)args;
    while (true)
    {
        int data = 0;
        rq->Pop(&data);
        cout << "消费数据是:" << data << endl;
        sleep(1);
    }
}

void *producter(void *args)
{
    RingQueue<int> *rq = (RingQueue<int> *)args;
    while (true)
    {
        int data = rand() % 20 + 1;
        cout << "生产数据是:" << data << endl;
        rq->Push(data);
        sleep(1);
    }
}

int main()
{
    srand((long long)time(nullptr));
    RingQueue<int> *rq = new RingQueue<int>();

    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, (void *)rq);
    pthread_create(&c, nullptr, producter, (void *)rq);

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

    return 0;
}

运行代码:
在这里插入图片描述

不过上诉代码是单生产者单消费者。我们只需要加两把互斥锁,分别用于生产者和消费者,就可以将其改为多生产者多消费者:

//ring_queue.hpp
#pragma once
#include <vector>
#include <iostream>
#include <semaphore.h>
#include <pthread.h>

//多生产者多消费者

using namespace std;

namespace ns_ring_queue
{
    const int g_cap_default = 10;
    template <class T>
    class RingQueue
    {
    private:
        vector<T> _ring_queue;
        int _cap;

        //生产者关心空位置资源
        sem_t blank_sem;
        //消费者关心数据资源
        sem_t data_sem;

        //生产者生产数据的位置
        int p_step;
        //消费者消费数据的位置
        int c_step;

        //加两把锁,实现多生产者多消费者
        pthread_mutex_t c_mtx;
        pthread_mutex_t p_mtx;

    public:
        RingQueue(int cap = g_cap_default)
            : _cap(cap), _ring_queue(cap), c_step(0), p_step(0)
        {
            sem_init(&blank_sem, 0, cap);
            sem_init(&data_sem, 0, 0);

            pthread_mutex_init(&c_mtx, nullptr);
            pthread_mutex_init(&p_mtx, nullptr);
        }

        ~RingQueue()
        {
            sem_destroy(&blank_sem);
            sem_destroy(&data_sem);

            pthread_mutex_destroy(&c_mtx);
            pthread_mutex_destroy(&p_mtx);
        }

        //生产
        void Push(const T &in)
        {
            //申请信号量
            sem_wait(&blank_sem); // P(空位置)
            //注意:lock放在wait之后
            pthread_mutex_lock(&p_mtx);
            //可以生产了,可是往哪个位置生产呢?
            _ring_queue[p_step] = in;
            sem_post(&data_sem); // V(数据)
            p_step++;
            p_step %= _cap;
            
            pthread_mutex_unlock(&p_mtx);
        }

        //消费
        void Pop(T *out)
        {
            sem_wait(&data_sem); // P(数据)

            //注意:lock放在wait之后
            pthread_mutex_lock(&c_mtx);
            *out = _ring_queue[c_step];
            sem_post(&blank_sem); // V(空位置)
            c_step++;
            c_step %= _cap;
            pthread_mutex_unlock(&c_mtx);
        }
    };
}


//ring_cp.cpp
#include "ring_queue1.hpp"
#include <pthread.h>
#include <time.h>
#include <unistd.h>
using namespace ns_ring_queue;

void *consumer(void *args)
{
    RingQueue<int> *rq = (RingQueue<int> *)args;
    while (true)
    {
        int data = 0;
        rq->Pop(&data);
        cout << "线程"
             << "[" << pthread_self() << "]"
             << "消费数据:" << data << endl;
        sleep(1);
    }
}

void *producter(void *args)
{
    RingQueue<int> *rq = (RingQueue<int> *)args;
    while (true)
    {
        int data = rand() % 20 + 1;
        cout << "线程"
             << "[" << pthread_self() << "]"
             << "生产数据:" << data << endl;
        rq->Push(data);
        sleep(1);
    }
}

int main()
{
    srand((long long)time(nullptr));
    RingQueue<int> *rq = new RingQueue<int>();

    pthread_t c1, c2, c3, p1, p2, p3;
    pthread_create(&c1, nullptr, consumer, (void *)rq);
    pthread_create(&c2, nullptr, consumer, (void *)rq);
    pthread_create(&c3, nullptr, consumer, (void *)rq);
    pthread_create(&p1, nullptr, producter, (void *)rq);
    pthread_create(&p2, nullptr, producter, (void *)rq);
    pthread_create(&p3, nullptr, producter, (void *)rq);

    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    pthread_join(c3, nullptr);
    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    pthread_join(p3, nullptr);

    return 0;
}

运行代码:
在这里插入图片描述

  • 35
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 51
    评论
评论 51
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值