【Linux操作系统】简学深悟启示录:线程同步与互斥

文章目录

1.样例引入

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>

using namespace std;

#define NUM 5

class threadData
{
public:
    threadData(int number)
    {
        threadname = "thread-" + to_string(number);
    }
public:
    string threadname;
};

int tickets = 1000;

void* getTicket(void* args)
{
    threadData* td = static_cast<threadData*>(args);
    while(true)
    {
        if(tickets > 0)
        {
            usleep(10000);
            printf("who = %s, get a ticket: %d\n", td->threadname.c_str(), tickets);
            tickets--;
        }
        else
        {
            break;
        }
    }
    printf("%s ... quit\n", td->threadname.c_str());
    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    vector<threadData*> thread_datas;
    for(int i = 1; i <= NUM; ++i)
    {
        pthread_t tid;
        threadData* td = new threadData(i);
        thread_datas.push_back(td);
        pthread_create(&tid, nullptr, getTicket, td);
        tids.push_back(tid);
    }
    
    for(int i = 0; i < tids.size(); ++i)
    {
        pthread_join(tids[i], nullptr);
    }
    for(int i = 0; i < thread_datas.size(); ++i)
    {
        delete thread_datas[i];
    }
}

这是一个模拟抢票系统的多线程代码核心功能是让 5 个线程竞争抢购 1000 张共享门票,本质是演示多线程对共享资源的并发访问

  1. 共享资源 :全局变量 tickets = 1000(代表 1000 张门票,所有线程共同争抢);
  2. 线程数据threadData 类,仅存储每个线程的名称(如 thread-1thread-2),用于区分抢票线程;
  3. 线程函数getTicket,每个线程的执行逻辑:
    • 循环判断是否还有剩余门票(tickets > 0);
    • 若有票,休眠 10 毫秒(模拟抢票前的耗时操作,如验证信息);
    • 打印抢票成功信息,并将门票数减 1;
    • 无票时退出循环,线程结束;
  4. 主线程逻辑
    • 创建 5 个线程,为每个线程分配独立的 threadData 实例(存线程名);
    • 等待所有线程执行完毕(pthread_join);
    • 释放 threadData 占用的内存,避免泄漏。

查看输出的日志,发现数据发生混乱,电影票数出现了负数,多个线程同时操作共享变量 tickets,但没有任何保护机制,会出现超卖、重复出票等异常(比如不同线程抢到同一张票,或最终门票数小于 0)

每条线程都要经过

  1. 将全局变量 tickets 读入 cpu
  2. cpu 内部进行计算
  3. 将计算结果返回内存的三个步骤

假如当 thread-1 刚将获取到的 tickets 放入 cpu 时,此时进行时间片轮转,将上下文保存,下一个 thread-2 可能获取到的 tickets 会有很大差异,对 tickets 进行计算并放回内存,然后又轮到 thread-1,此时将上下文放回 cpu 计算,但是该上下文的 tickets 是个旧数据,没有被及时更新,这就导致了数据混乱了

那么该如何解决这一问题呢?这就涉及多线程的锁问题了!

2.线程互斥

2.1 互斥相关概念

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

2.2 mutex互斥锁操作

cpp 复制代码
class threadData
{
public:
    threadData(int number, pthread_mutex_t* mutex)
    {
        threadname = "thread-" + to_string(number);
        lock = mutex;
    }
public:
    string threadname;
    pthread_mutex_t* lock;
};

void* getTicket(void* args)
{
    threadData* td = static_cast<threadData*>(args);
    while(true)
    {
        pthread_mutex_lock(td->lock);
        if(tickets > 0)
        {
            usleep(10000);
            printf("who = %s, get a ticket: %d\n", td->threadname.c_str(), tickets);
            tickets--;
            pthread_mutex_unlock(td->lock);
        }
        else
        {
            pthread_mutex_unlock(td->lock);
            break;
        }
        usleep(15);
    }
    printf("%s ... quit\n", td->threadname.c_str());
    return nullptr;
}

int main()
{
    pthread_mutex_t lock;
    pthread_mutex_init(&lock, nullptr);
    
	.......
	
    pthread_mutex_destroy(&lock);
}

我们将通过修改线程的执行函数来展示互斥的具体表现

这里创建互斥锁 lock 并通过 pthread_mutex_init 初始化锁,每个线程在模拟抢票之前需要 pthread_mutex_lock 竞争一把锁,抢到锁的人才能够执行模拟抢票,而其他线程只能阻塞,等待锁释放后重新抢夺锁,但是每个线程对于锁的竞争能力都有所细微差别,当同一个线程释放完锁可能会立马再次抢夺互斥锁,所以通过 usleep 模拟 pthread_cond_wait 进行队列等待,防止锁的过度占据,这部分后面会讲,最后 pthread_mutex_destroy 释放锁

可以看到确实是不会出现数据混乱不合理的情况了,加锁的本质 就是用时间换空间,加锁的表现 就在于线程对于临界区代码的执行,不过加锁的临界区还是有原则 的,保证临界区代码越少越好,所以我们说一个线程要么持有锁,要么释放锁,这是原子性的,非 01

纯互斥环境,如果锁的分配不够合理,那么锁一直被一个线程占据,就会导致其他线程长时间拿不到锁,这叫线程的饥饿问题,不是说有互斥必有饥饿问题,也是有适合纯互斥的环境的

临界区中可以进行线程切换吗?

可以的,此时线程切换,执行其他时间片,也是阻塞状态,所以看起来好像不能线程切换,简单类比:你进房间(临界区)后锁上门(获取锁),即使暂时离开座位(线程切换),门依然锁着,其他人只能在门外等,直到你回来开门(释放锁)

🔥值得注意的是: pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER 宏定义自动对全局锁 lock 初始化,无需调用 pthread_mutex_init

2.3 互斥的原理

为了实现互斥锁操作,大多数体系结构都提供了 swapexchange 指令。该指令的作用是将寄存器和内存单元的数据进行交换,由于仅包含一条指令,因此保证了操作的原子性。即使在多处理器平台中,访问内存的总线周期也存在先后顺序:当一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期结束后再执行。现在,我们将 lockunlock 的伪代码修改如下:

在单核 CPU 上,所有线程共享同一组物理寄存器 ,但通过操作系统的上下文切换(Context Switch) 机制,实现了逻辑上的隔离,以下将对 lockunlock 进行解释:

2.3.1 lock

初始状态 :内存中的全局变量 mutex = 1

1.线程 A 执行(获取锁成功)
CPU 正在执行线程 A 的指令流:

  • movb $0, %alCPU 将物理寄存器 %al 的值置为 0
  • xchgb %al, mutexCPU 执行原子交换,mutex 变为 0,物理寄存器 %al 变为 1
  • if (al > 0)CPU 检查 %al,值为 1,条件成立,线程 A 进入临界区代码段

2.发生上下文切换(中断/时间片耗尽)

此时,时钟中断触发,操作系统介入调度:

操作系统将当前物理寄存器 %al 的值(即 1 )保存到线程 A 的内存 TCB 中,从线程 BTCB 中恢复数据到物理寄存器(此时 %al 变成了 B 之前保存的值,或者是初始垃圾值)。CPU 开始执行线程 B 的指令

3.线程 B 执行(获取锁失败)

现在 CPU 属于线程 B,但必须注意:内存中的 mutex 此时是 0(被 A 改写的)

  • movb $0, %al:线程 B 将物理寄存器 %al 置为 0。(注意:这覆盖了刚恢复的寄存器值,但不影响内存里 A 保存的上下文)
  • xchgb %al, mutexCPU 执行原子交换,物理寄存器 %al(值为 0)和 内存 mutex(值为 0),mutex 保持 0,物理寄存器 %al 读入 0
  • if (al > 0)CPU 检查 %al,值为 0,条件不成立
  • else:线程 B 执行挂起(suspend)操作,主动让出 CPU

4.切回线程 A(恢复现场)

操作系统再次调度:

操作系统从线程 ATCB 中读取数据,写回 CPU 物理寄存器,此时物理寄存器 %al 被恢复为 1 (这是步骤 2 中保存的值),线程 A 继续在临界区执行,稍后执行 unlock 将内存 mutex 恢复为 1

总结:

  1. 物理层 :只有一个 %al 寄存器,AB不同时间段独占使用它。
  2. 逻辑层 :通过保存/恢复上下文 ,线程 A 看到的 %al 值(1)被保存在了 A 的私有内存空间中,从未丢失。
  3. 共享数据mutex 位于全局堆内存或数据段,不随上下文切换而改变,始终维持最新状态(0),从而实现了 AB 的阻塞。

2.3.2 unlock

lock 的过程,此时 mutex = 0(锁被占用)

  • movb $1, mutex(释放锁):CPU 直接将立即数 1 写入到 mutex 的内存地址中,全局变量 mutex0 变回 1,此时锁在物理上已经空闲了,但线程 B 还在睡觉,根本不知道这件事

  • 唤醒等待 Mutex 的线程(系统调用):线程 A 发起系统调用(如 Linux 中的 futex_wake),然后操作系统介入:

    • 操作系统查看关联该 mutex 的等待队列
    • 找到处于阻塞状态的线程 B
    • 将线程 B 的状态从 BLOCKED 改为 READY(就绪态)
    • 将线程 B 放入调度器的就绪队列(Run Queue
      注意 :此时线程 B 只是有了被执行的资格,并没有立即抢占 CPU。线程 A 可能继续运行直到时间片用完,或者根据调度策略,OS 决定立刻调度 B
  • 保存线程 A 的当前寄存器状态,恢复线程 B 的寄存器状态和程序计数器(PC/EIP)。线程 B 之前是在 lock 函数的 else 分支里挂起的。根据代码逻辑,唤醒后的下一条指令通常是 goto lock(或者循环回头部)

2.4 互斥锁的封装

cpp 复制代码
#pragma once

#include <pthread.h>


class Mutex
{
public:
    Mutex(pthread_mutex_t* lock)
    : lock_(lock)
    {}

    void Lock()
    {
        pthread_mutex_lock(lock_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(lock_);
    }

    ~Mutex()
    {}
private:
    pthread_mutex_t* lock_; 
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t* lock)
    : mutex_(lock)
    {
        mutex_.Lock();
    }
    ~LockGuard()
    {
        mutex_.Unlock();
    }
private:
    Mutex mutex_;
};

Mutex 类封装 lockunlock 方法,然后再创建 LockGuard 类,在构造和析构函数中使用这两个方法,进而能够实现 RALL 风格的互斥锁

C++11 标准中,RAII 风格的互斥锁可通过 标准库自带的 std::mutex(互斥锁)+ std::lock_guard(锁守卫) 实现,无需手动封装(底层已原生支持 RAII)。相比 POSIX pthread 库的手动加锁 / 解锁,C++11 标准库的方案更简洁、跨平台,且能避免死锁风险

3.线程同步

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

3.1 条件变量

从概念上面互斥锁的例子我们可以知道,即使是上了互斥锁保证数据的一个一个输出,但是各个线程的优先级仍然有差距,一个锁过度的抢占锁也会出现问题,所以衍生出了条件变量的概念

举个例子:

排队进入房间需要一把钥匙(锁),拿到钥匙就可以进入房间(lock),不会被人打扰,但是只要你不想使用这个房间,出了房间后必须把钥匙挂回去(unlock),要么离开这里(销毁线程),要么重新到一个名为 task_struct *wait_queue 的队列排队重新拿到钥匙,无论选择这两种的哪一种,都需要敲一下钟提醒下一个人拿钥匙进房间(唤醒正在堵塞的一个或全部线程),以此循环

那么这个钟和队列我们就称其为条件变量,它必须与互斥锁(Mutex) 配合使用

3.2 条件变量函数

条件变量的函数和互斥锁的函数很像,直接查看就能明白


参数:

  • cond: 指向要初始化的 pthread_cond_t 结构体(即条件变量本身)的指针
  • attr: 一个可选的指针,指向 pthread_condattr_t 结构体,用于指定条件变量的属性。如果 attrNULL,则条件变量将使用默认属性进行初始化(这是最常见的使用方式)

用于销毁一个通过 pthread_cond_init 动态创建的条件变量


参数:

  • cond: 指向要等待的条件变量 (pthread_cond_t) 的指针
  • mutex: 指向与该条件变量关联的互斥锁 (pthread_mutex_t) 的指针。在调用此函数时,调用线程必须已经持有这个互斥锁

这两个函数都是用于唤醒正在等待互斥锁的线程,signal 唤醒一个,broadcast 唤醒队列里所有线程

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>


pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int cnt = 0;

void* Count(void* arg)
{
    uint64_t id = (uint64_t)arg;
    pthread_detach(pthread_self());
    std::cout << "pthread: " << id << " , creat success" << std::endl;
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);
        std::cout << "pthread: " << id << " , cnt: " << cnt++ << std::endl;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    for(uint64_t i = 0; i < 5; ++i)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, Count, (void*)i);
        usleep(1000);
    }

    sleep(3);
    std::cout << "main thread ctrl begin: " << std::endl;
    while(true)
    {
        sleep(1);
        pthread_cond_signal(&cond);
      //pthread_cond_broadcast(&cond);
        std::cout << "signal one thread..." << std::endl;
    }
}

创建 5 个线程,然后进入对应的线程实现方法,pthread_detach 设置分离状态自动回收线程,pthread_mutex_lock 上锁后,pthread_cond_wait 条件变量阻塞,到这一步每个线程都要进入队列排队,有人说这拿着锁去排队不会死锁吗,其实设计的时候早就考虑过了,这里 pthread_cond_wait 排队会自动释放锁

此时所有创建的线程都会在队列中排队,主线程执行到 pthread_cond_signalpthread_cond_broadcast 唤醒队列中的线程,然后线程它会尝试重新获取互斥锁,被分配到锁从 pthread_cond_wait 那一步继续往下执行,依次循环往复

4.生产消费模型

4.1 概念解析

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

生产消费模型支持"321原则":

  • 3个关系:
    • 生产者vs生产者: 互斥
    • 消费者vs消费者: 互斥
    • 生产者vs消费者: 互斥,同步(因为不能让生产或消费一方过度占据阻塞队列的使用权)
  • 2个角色: 生产者和消费者
  • 1个场景: 阻塞队列

4.2 单生产单消费

main.cpp

cpp 复制代码
#include "blockqueue.hpp"
#include "Task.hpp"

void* Consumer(void* arg)
{
    Blockqueue<Task>* bq = static_cast<Blockqueue<Task>*>(arg);
    while(true)
    {
        Task t = bq->pop();
        t.run();
        std::cout << "消费了一个任务:" << t.GetResult() << std::endl;
    }
}  

void* Producer(void* arg)
{
    int len = opers.size();
    Blockqueue<Task>* bq = static_cast<Blockqueue<Task>*>(arg);
    while(true)
    {
        int data1 = rand() % 10 + 1;
        int data2 = rand() % 10;
        char op = opers[rand() % len];
        Task t(data1, data2, op);
        bq->push(t);
        std::cout << "生产了一个任务:" << t.GetTask() << std::endl;
        sleep(1);
    }
}

int main()
{
    Blockqueue<Task>* bq = new Blockqueue<Task>();
    pthread_t c, p;
    pthread_create(&c, nullptr, Consumer, bq);
    pthread_create(&p, nullptr, Producer, bq);

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

Task.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>

std::string opers = "+-*/%";

enum
{
    DivZero = 1,
    ModZero,
    Unknown,
};

class Task
{
public:
    Task(int data1, int data2, char op)
    : data1_(data1)
    , data2_(data2)
    , op_(op)
    , result_(0)
    , exitcode_(0)
    {}

    void run()
    {
        switch(op_)
        {
            case '+':
                result_ = data1_ + data2_;
                break;
            case '-':
                result_ = data1_ - data2_;
                break;
            case '*':
                result_ = data1_ * data2_;
                break;
            case '/':
                {
                    if(data2_ == 0) exitcode_ = DivZero;
                    else result_ = data1_ / data2_;
                }
                break;
            case '%':
                {
                    if(data2_ == 0) exitcode_ = ModZero;
                    else result_ = data1_ % data2_;
                }
                break;
            dafalt:
                exitcode_ = Unknown;
                break;
        }
    }

    std::string GetResult()
    {
        std::string ret = std::to_string(data1_);
        ret += op_;
        ret += std::to_string(data2_);
        ret += "=";
        ret += std::to_string(result_);
        ret += "[code: ";
        ret += std::to_string(exitcode_);
        ret += "]";
        return ret;
    }

    std::string GetTask()
    {
        std::string ret = std::to_string(data1_);
        ret += op_;
        ret += std::to_string(data2_);
        ret += "=?";
        return ret;
    }

    ~Task()
    {}
private:
    int data1_;
    int data2_;
    char op_;
    int result_;
    int exitcode_;
};

blockqueue.hpp

cpp 复制代码
#pragma once

#include <pthread.h>
#include <iostream>
#include <queue>
#include <unistd.h>

template <class T>
class Blockqueue
{
    static const int defalutnum = 15;
public:
    Blockqueue(int maxcap = defalutnum)
    : maxcap_(maxcap)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&c_cond_, nullptr);
        pthread_cond_init(&p_cond_, nullptr);
        low_water_ = maxcap_ / 4;
        high_water_ = maxcap_ * 3 / 4;
    }

    T pop()
    {
        pthread_mutex_lock(&mutex_);
        while(q_.size() == 0)
        {
            pthread_cond_wait(&c_cond_, &mutex_);
        }
        T out = q_.front();
        q_.pop();
        if(q_.size() < low_water_)pthread_cond_signal(&p_cond_);
        pthread_mutex_unlock(&mutex_);
        return out;
    }

    void push(const T& in)
    {
        pthread_mutex_lock(&mutex_);
        while(q_.size() == maxcap_)
        {
            pthread_cond_wait(&p_cond_, &mutex_);
        }
        q_.push(in);
        if(q_.size() > high_water_)pthread_cond_signal(&c_cond_);
        pthread_mutex_unlock(&mutex_);
    }

    ~Blockqueue()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&c_cond_);
        pthread_cond_destroy(&p_cond_);
    }
private:
    std::queue<T> q_;
    int maxcap_;
    pthread_mutex_t mutex_;
    pthread_cond_t c_cond_;
    pthread_cond_t p_cond_;
    int low_water_;
    int high_water_;
};

这是阻塞队列的类实现,设置一把锁 mutex_ 保证互斥关系,给生产消费各自设置一个条件变量 c_cond_p_cond_,以及设置高低水位线 low_water_high_water_ 保证及时补充消费资源,maxcap_ 表示阻塞队列的最大容量

poppush 表示往阻塞队列消费和生产资源,实现逻辑差不多,代码不是很难,但是细节很多

  • 临界区必须把 while 循环也包含进去,而不是生产消费的时候才加锁,因为比如 while 循环中的 q_.size() == 0 判断也是需要访问临界区的,只要是访问临界区就需要加锁
  • 这里的 while 循环是为了保证不过度消费和生产,当 q_.size()==0 时,阻塞队列为空,那么消费者就需要阻塞;当 q_.size()==maxcap_ 时,阻塞队列满了,那么生产者就需要阻塞等待
  • 使用 while 而不是 if 是为了避免伪唤醒的情况出现,比如多线程当还有一个资源就满时,调用pthread_cond_broadcast 唤醒而不是 pthread_cond_signal,这是正在阻塞队列的线程全部被唤醒,被唤醒队列的第一个线程生产一个资源后,下一个线程再次拿到锁生产一个资源时,此时阻塞队列已经满了,再添加资源就会出现错误,因此需要 while 循环多次判断当前阻塞队列情况
  • 生产者的唤醒操作由消费者决定,当 q_.size() < low_water_ 时,资源即将消费完,消费者唤醒生产者生产资源;消费者的唤醒操作由生产者决定,当 q_.size() > high_water_ 时,资源即将填满,生产者唤醒消费者消费资源

4.3 多生产多消费

cpp 复制代码
int main()
{
    Blockqueue<Task>* bq = new Blockqueue<Task>();
    pthread_t c[3], p[5];
    for(int i = 0; i < 3; ++i)
    {
        pthread_create(&c[i], nullptr, Consumer, bq);
    }

    for(int i = 0; i < 5; ++i)
    {
        pthread_create(&p[i], nullptr, Producer, bq);
    }

    for(int i = 0; i < 3; ++i)
    {
        pthread_join(c[i], nullptr);
    }                   
    for(int i = 0; i < 5; ++i)
    {
        pthread_join(p[i], nullptr);    
    }
    delete bq;
    return 0;
}

只需要在创建的时候多创建几个就好了,用的都是同一个阻塞队列和互斥锁

5.POSIX信号量

上面普通的生产消费模型,虽然能满足一般的情况,但是他的资源只有一份,并不能很好的保证多并发的情况,因此我们使用信号量可以很好的处理这种情况

信号量简单理解就是一把计数器,计数器可以保证使用共享资源的执行流数量,他的操作是 PV 原子的,就像我们在电影院看电影,需要我们提前订票预定座位,信号量就是一种对资源的预定机制

5.1 生产消费模型环形队列

环形队列中有一个 head 指针和 tail 指针,生产者关心的是还有多少空闲资源,消费者关心的是还有多少资源能使用,当两个指针指向同一位置的时候只有队列为空或队列为满两种情况

5.2 单生产单消费

main.cpp

cpp 复制代码
#include "RingQueue.hpp"

using namespace std;

void* Producer(void* arg)
{
    RingQueue<int>* rq = static_cast<RingQueue<int>*>(arg);
    while(true)
    {
        int data = rand() % 10 + 1;
        rq->Push(data);
        cout << "Producer data done, data is: " << data << endl;
        sleep(1);
    }
    return nullptr;
}

void* Consumer(void* arg)
{
    sleep(2);
    RingQueue<int>* rq = static_cast<RingQueue<int>*>(arg);
    while(true)
    {
        int data = rq->Pop();
        cout << "Consumer get data , data is: " << data << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    srand((unsigned int)time(nullptr) ^ getpid());
    RingQueue<int>* rq = new RingQueue<int>();
    pthread_t c, p;
    
    pthread_create(&p, nullptr, Producer, rq);
    pthread_create(&c, nullptr, Consumer, rq);

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

    return 0;
}

RingQueue.hpp

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
#include <semaphore.h>  


template <class T>
class RingQueue
{
    const static int defaultcap = 10;
public:
    RingQueue(int maxcap_ = defaultcap)
    : ringqueue_(maxcap_)
    , maxcap_(maxcap_)
    , c_step_(0)
    , p_step_(0)
    {
        sem_init(&cdata_sem_, 0, 0);
        sem_init(&pspace_sem_, 0, maxcap_);
    }

    void P(sem_t& pspace_sem_)
    {
        sem_wait(&pspace_sem_);
    }

    void V(sem_t& cdata_sem_)
    {
        sem_post(&cdata_sem_);
    }

    void Push(const T& in)
    {
        P(pspace_sem_);
        ringqueue_[p_step_] = in;
        p_step_ = (p_step_ + 1) % maxcap_;
        V(cdata_sem_);
    }

    T Pop()
    {
        P(cdata_sem_);
        T out = ringqueue_[c_step_];
        c_step_ = (c_step_ + 1) % maxcap_;
        V(pspace_sem_);
        return out;
    }

    ~RingQueue()
    {
        sem_destroy(&cdata_sem_);
        sem_destroy(&pspace_sem_);
    }
private:
    std::vector<T> ringqueue_;
    int maxcap_;

    int c_step_;
    int p_step_;

    sem_t cdata_sem_;
    sem_t pspace_sem_;
};

这是实现环形队列的类 RingQueue,该环形队列使用数组模拟的,通过除模来实现循环,信号量的使用和锁基本一致,sem_t cdata_sem_sem_t pspace_sem_ 分别表示消费者和生产者的下标,P 操作的 sem_wait 表示申请资源,V 操作的 sem_post 表示释放资源

5.3 多生产多消费

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
#include <semaphore.h>  


template <class T>
class RingQueue
{
    const static int defaultcap = 10;
public:
    RingQueue(int maxcap_ = defaultcap)
    : ringqueue_(maxcap_)
    , maxcap_(maxcap_)
    , c_step_(0)
    , p_step_(0)
    {
        sem_init(&cdata_sem_, 0, 0);
        sem_init(&pspace_sem_, 0, maxcap_);
        pthread_mutex_init(&c_mutex_, nullptr);
        pthread_mutex_init(&p_mutex_, nullptr);
    }

    void P(sem_t& pspace_sem_)
    {
        sem_wait(&pspace_sem_);
    }

    void V(sem_t& cdata_sem_)
    {
        sem_post(&cdata_sem_);
    }

    void Lock(pthread_mutex_t& mutex)
    {
        pthread_mutex_lock(&mutex);
    }

    void Unlock(pthread_mutex_t& mutex)
    {
        pthread_mutex_unlock(&mutex);
    }

    void Push(const T& in)
    {
        P(pspace_sem_);
        Lock(p_mutex_);
        ringqueue_[p_step_] = in;
        p_step_ = (p_step_ + 1) % maxcap_;
        Unlock(p_mutex_);
        V(cdata_sem_);
    }

    T Pop()
    {
        P(cdata_sem_);
        Lock(c_mutex_);
        T out = ringqueue_[c_step_];
        c_step_ = (c_step_ + 1) % maxcap_;
        Unlock(c_mutex_);
        V(pspace_sem_);
        return out;
    }

    ~RingQueue()
    {
        sem_destroy(&cdata_sem_);
        sem_destroy(&pspace_sem_);
        pthread_mutex_destroy(&c_mutex_);
        pthread_mutex_destroy(&p_mutex_);
    }
private:
    std::vector<T> ringqueue_;
    int maxcap_;

    int c_step_;
    int p_step_;

    sem_t cdata_sem_;
    sem_t pspace_sem_;

    pthread_mutex_t c_mutex_;
    pthread_mutex_t p_mutex_;
};

多生产多消费唯一的区别在于需要加锁,因为多个资源对同一信号量进行操作可能会出问题,创建使用锁的操作都很简单,但是这里有处细节,是先申请锁还是先申请信号量呢?逻辑上来说确实是先申请锁再申请信号量,但是这从技术角度来说并不好。应该先申请信号量,预定机制并不会妨碍锁的竞争,只要锁一放出来立马就有人拿到并使用,先申请锁会把其他线程堵在外面,就算拿到锁也要花时间申请信号量

6.线程池

Task.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>

std::string opers = "+-*/%";

enum
{
    DivZero = 1,
    ModZero,
    Unknown,
};

class Task
{
public:
    Task(int data1, int data2, char op)
    : data1_(data1)
    , data2_(data2)
    , op_(op)
    , result_(0)
    , exitcode_(0)
    {}

    void run()
    {
        switch(op_)
        {
            case '+':
                result_ = data1_ + data2_;
                break;
            case '-':
                result_ = data1_ - data2_;
                break;
            case '*':
                result_ = data1_ * data2_;
                break;
            case '/':
                {
                    if(data2_ == 0) exitcode_ = DivZero;
                    else result_ = data1_ / data2_;
                }
                break;
            case '%':
                {
                    if(data2_ == 0) exitcode_ = ModZero;
                    else result_ = data1_ % data2_;
                }
                break;
            dafalt:
                exitcode_ = Unknown;
                break;
        }
    }

    std::string GetResult()
    {
        std::string ret = std::to_string(data1_);
        ret += op_;
        ret += std::to_string(data2_);
        ret += "=";
        ret += std::to_string(result_);
        ret += "[code: ";
        ret += std::to_string(exitcode_);
        ret += "]";
        return ret;
    }

    std::string GetTask()
    {
        std::string ret = std::to_string(data1_);
        ret += op_;
        ret += std::to_string(data2_);
        ret += "=?";
        return ret;
    }

    ~Task()
    {}
private:
    int data1_;
    int data2_;
    char op_;
    int result_;
    int exitcode_;
};

main.cpp

cpp 复制代码
#include "ThreadPool.hpp"
#include "Task.hpp"

using namespace std;

int main()
{
    srand((unsigned int)time(nullptr) ^ getpid());
    ThreadPool<Task>* tp = new ThreadPool<Task>(10);
    tp->Start();
    while(true)
    {
        int data1 = rand() % 100;
        int data2 = rand() % 100;
        char op = opers[rand() % opers.size()];
        Task t(data1, data2, op);
        tp->Push(t);
        std::cout << "main thread make task: " << t.GetTask() << std::endl;
        sleep(1);
    }
    return 0;
}

ThreadPool.hpp

cpp 复制代码
#pragma once
#include <vector>
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <queue>
#include <string>
#include "Task.hpp"

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

template <class T>
class ThreadPool
{
    static const int defaultnum = 5;
private:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }
    void Wait()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid)
    {
        for(int i = 0; i < threads_.size(); ++i)
        {
            if(pthread_equal(threads_[i].tid, tid))
            {
                return threads_[i].name;
            }
        }
        return "unknown";
    }
    T pop()
    {
        T task = tasks_.front();
        tasks_.pop();
        return task;
    }
public:
    ThreadPool(int num = defaultnum)
    : threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr); 
    }

    static void* HandlerTask(void* arg)
    {
        ThreadPool<T>* tp = static_cast<ThreadPool<T>*>(arg);
        std::string threadname = tp->GetThreadName(pthread_self());
        while(true)
        {
            tp->Lock();
            while(tp->IsQueueEmpty())
            {
                tp->Wait();
            }
            T task = tp->pop();
            tp->Unlock();
            task.run();
            std::cout << threadname << " result: " << task.GetResult() << std::endl;
        }
    }

    void Start()
    {
        for(int i = 0; i < threads_.size(); ++i)
        {
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
            threads_[i].name = "thread-" + std::to_string(i);
        }
    }

    void Push(const T& task)
    {
        Lock();
        tasks_.push(task);
        Unlock();
        Wakeup();
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
private:
    std::vector<ThreadInfo> threads_;
    std::queue<T> tasks_;
    pthread_mutex_t mutex_;
    pthread_cond_t cond_;
};

线程池实际上是前面知识的最终汇总,关键在于对于各个接口的封装和细节处理,vector<ThreadInfo> threads_ 存放线程,queue<T> tasks_ 为任务队列,最重要的是 static void* HandlerTask(void* arg) 的线程实现,因为他的参数只能是 void*,但是她又是放在类内作为成员函数,所以必须传递 this 指针绑定对象,我们无法实现,所以只能将其作为 static 静态成员处理

7.线程安全的单例模式

单例模式分为懒汉和饿汉,无非就是延迟加载和提前加载单例的区别,考虑到内存优化,这里我们用懒汉模式对线程池进一步优化

main.cpp

cpp 复制代码
#include "ThreadPool.hpp"
#include "Task.hpp"


int main()
{
    std::cout << "ThreadPool test start!" << std::endl;
    srand((unsigned int)time(nullptr) ^ getpid());
    ThreadPool<Task>::GetThreadPool()->Start();
    while(true)
    {
        int data1 = rand() % 100;
        int data2 = rand() % 100;
        char op = opers[rand() % opers.size()];
        Task t(data1, data2, op);
        ThreadPool<Task>::GetThreadPool()->Push(t);
        std::cout << "main thread make task: " << t.GetTask() << std::endl;
        sleep(1);
    }
    return 0;
}

ThreadPool.hpp

cpp 复制代码
#pragma once
#include <vector>
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <queue>
#include <string>
#include "Task.hpp"

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

template <class T>
class ThreadPool
{
    static const int defaultnum = 5;

private:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }
    void Wait()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid)
    {
        for (int i = 0; i < threads_.size(); ++i)
        {
            if (pthread_equal(threads_[i].tid, tid))
            {
                return threads_[i].name;
            }
        }
        return "unknown";
    }
    T pop()
    {
        T task = tasks_.front();
        tasks_.pop();
        return task;
    }

public:
    static void *HandlerTask(void *arg)
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(arg);
        std::string threadname = tp->GetThreadName(pthread_self());
        while (true)
        {
            tp->Lock();
            while (tp->IsQueueEmpty())
            {
                tp->Wait();
            }
            T task = tp->pop();
            tp->Unlock();
            task.run();
            std::cout << threadname << " result: " << task.GetResult() << std::endl;
        }
    }

    void Start()
    {
        for (int i = 0; i < threads_.size(); ++i)
        {
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
            threads_[i].name = "thread-" + std::to_string(i);
        }
    }

    void Push(const T &task)
    {
        Lock();
        tasks_.push(task);
        Unlock();
        Wakeup();
    }
    
    static ThreadPool<T> *GetThreadPool()
    {
        if (tp_ == nullptr)
        {
            pthread_mutex_lock(&lock_);
            if (tp_ == nullptr)
            {
                std::cout << "log: singleton threadpool create!" << std::endl;
                tp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&lock_);
        }
        return tp_;
    }

private:
    ThreadPool(int num = defaultnum)
        : threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    ThreadPool(const ThreadPool<T> &) = delete;
    ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;

private:
    std::vector<ThreadInfo> threads_;
    std::queue<T> tasks_;
    pthread_mutex_t mutex_;
    pthread_cond_t cond_;

    static ThreadPool<T> *tp_;
    static pthread_mutex_t lock_;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

创建两个静态全局变量 static ThreadPool<T> *tp_static pthread_mutex_t lock_,唯一实例 tp_ 只能通过 static ThreadPool<T> *GetThreadPool() 获取,再把构造析构赋值函数进行私有化,避免外部多次创建实例,这里加锁为了避免多个线程同时涌入导致创建多个实例,再通过内外两层 if 语句判断实现创建一次实例,就不用再去申请锁判断是否要创建实例

8.死锁

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

申请一把锁是原子的,但是申请两把锁就不一定了

死锁四个必要条件:

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

避免死锁可以使用银行家算法死锁检测算法,这里不展开讲

还有悲观锁,乐观锁,自旋锁,读写锁等将会在额外拓展部分讲解

9.STL、智能指针和线程安全

STL中的容器是否是线程安全的?

不是。原因是,STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且对于不同的容器,加锁方式的不同,性能可能也不同(例如 hash 表的锁表和锁桶)。因此 STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全

智能指针是否是线程安全的?

对于 unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题。对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效、原子地操作引用计数


希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

相关推荐
2501_915921431 小时前
Fiddler抓包工具详解,HTTPHTTPS调试、代理配置与接口分析实战教程
服务器·ios·小程序·fiddler·uni-app·php·webview
要站在顶端1 小时前
Jenkins动态绑定节点设备ID:多物理机USB设备适配方案
运维·jenkins·cocoa
hhwyqwqhhwy1 小时前
linux 驱动iic
linux·运维·服务器
知识分享小能手1 小时前
CentOS Stream 9入门学习教程,从入门到精通, CentOS Stream 9中的文件和目录管理(3)
linux·学习·centos
Sally_xy1 小时前
使用 Jenkins
运维·jenkins
一只努力学习的Cat.1 小时前
Linux:NAPT等其他补充内容
linux·运维·网络
提笔忘字的帝国1 小时前
解决“该jenkins 实例似乎已离线“的问题
运维·jenkins
摸鱼仙人~1 小时前
VMware配置从开始踩坑总结-2025最新
linux·ubuntu
做咩啊~1 小时前
CentOS 7部署OpenLDAP+phpLDAPadmin实现统一认证
linux·运维·centos