Linux操作系统多线程同步

本文详细介绍了多线程编程中的线程同步概念,包括线程互斥可能导致的饥饿问题和如何通过条件变量解决。接着阐述了生产者消费者模型的原理和应用,并展示了如何使用阻塞队列实现。此外,还讲解了POSIX信号量在资源管理中的作用。最后,讨论了线程池的设计,并给出了简单的线程池实现示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、线程同步

1.饥饿问题

线程互斥这种现象是正确的,因为如果没有线程互斥的话,不能保证多个线程访问临界资源时数据的一致性。但是线程互斥也是不合理的,多个线程中有可能存在一个线程优先级很高,一个线程优先级又很低,在这些线程竞争锁资源的时候,优先级很高的那个线程有可能一直都抢到了锁,偶尔让优先级比它低一点的线程抢到了锁,但优先级很低的那个线程却有可能一直都抢不到锁。这就好比我们去食堂打饭,如果食堂只有一个打饭阿姨并且食堂规定同一时间只能给一个人打饭,不用排队,谁能抢到打饭的位置就给谁打饭。那么就会导致力量很强大的同学总是能打得到饭,而力量很弱小的同学始终不能打到饭。

这其实就是饥饿问题,指的是存在一个执行流长时间得不到某种资源。线程互斥就有可能会导致饥饿问题。但互斥不是错误的,它只是不合理。线程互斥的使用要看使用的场景,它比较适合于运用在一种突发情况,我们要进行无任何优先级的竞争时,比如说抢票这种场景下,要求每个线程的优先级都相同,用线程互斥的方式来实现是可以的。

2.线程同步的概念

想要解决线程互斥带来的饥饿问题,我们还可以实现线程同步。线程同步的概念是在保证临界资源安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效地避免了饥饿问题。就比如在多个线程中有一个优先级很高的线程和一个优先级很低的线程,可能刚开始申请锁资源的时候确实是按照优先级来分配,先分配给优先级最高的线程,但当优先级最高的线程解锁了以后,这个优先级最高的线程不能立即重新申请锁,必须排在队列的末尾重新按顺序获取锁资源。

3.条件变量

要想实现线程同步,我们就需要使用到条件变量,在没有使用条件变量的时候,所有线程都一窝蜂地去竞争锁资源,没有一个机制能够管理起这些线程来有序地竞争锁资源。所以条件变量是一种代码策略,我们使用条件变量以后,就可以控制哪些线程被唤醒去使用锁资源。下面我们先通过函数接口和模拟代码来简单地认识一下条件变量。

Linux操作系统下的条件变量是pthread_cond_t类型的,条件变量可以定义为全局的也可以定义为局部的,我们可以用 PTHREAD_COND_INITIALIZER 宏来进行初始化。条件变量必须与互斥锁一并使用。

// 用宏初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

条件变量的函数接口使用起来与互斥锁的函数接口非常相像,下面分别介绍一下条件变量函数接口的使用:

pthread_cond_init函数:
pthread_cond_init函数是条件变量的初始化函数,一般用于定义局部条件变量的初始化。这个参数也是非常简单,第一个参数cond传递定义好的条件变量,第二个参数attr设置条件变量的属性,一般我们设置为nullptr代表默认即可。

在这里插入图片描述

pthread_cond_destroy函数:
pthread_cond_destroy函数是释放条件变量的函数,在我们不再需要使用条件变量的时候,调用该函数将指定的条件变量释放,参数传递条件变量即可。

在这里插入图片描述

pthread_cond_wait函数:
pthread_cond_wait函数可以设置线程等待条件满足,如果不满足的时候线程会阻塞式的等待。第一个参数cond传递指定的条件变量,代表在该条件变量上等待。第二个参数传递指定的互斥锁,这个函数需要传递互斥锁的原因是当线程在持有互斥锁的时候被阻塞等待条件变量,会自动解锁给其它线程使用(不能该线程在等待条件变量的时候还持有着锁,这样其它线程就没办法获得锁资源了),当阻塞结束的时候,该函数也会自动帮助线程重新获得互斥锁,然后才返回。

在这里插入图片描述

pthread_cond_timedwait函数:
pthread_cond_timedwait函数和pthread_cond_wait函数功能类似,只不过pthread_cond_timedwait函数可以指定等待的时间是多久,最后一个参数abstime传递等待的时间。时间到了如果线程还没有被唤醒,就会自动醒来了。

在这里插入图片描述

pthread_cond_signal函数:
pthread_cond_signal函数与信号里的signal函数不同,pthread_cond_signal函数是用来唤醒某个线程的。它可以唤醒在指定条件变量下等待的线程。参数传递指定的条件变量即可。

在这里插入图片描述

pthread_cond_broadcast函数:
pthread_cond_broadcast函数也是唤醒在指定条件变量下等待的线程,只不过它是唤醒在等待的所有线程,参数也是传递指定的条件变量即可。

在这里插入图片描述

下面我们写一段小代码来演示一下这些函数接口的具体使用方式,我们创建3个新线程使用条件变量,用主线程来唤醒正在等待条件变量的线程。

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

using namespace std;

// 定义全局的条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 定义全局的互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 定义全局退出变量
volatile bool threadQuit = false;

void *waitCommand(void *args)
{
    char *name = (char *)args;

    while (!threadQuit)
    {
        // 由于pthread_cond_wait会自动释放锁,在被唤醒的时候又会自动获取锁
        // 所以需要在调用前加锁,调用后解锁
        // 这样才能确保退出的时候退出得干净
        pthread_mutex_lock(&mutex);
        cout << "这是 " << name << " 等待条件变量" << endl;
        pthread_cond_wait(&cond, &mutex);
        pthread_mutex_unlock(&mutex);

        cout << name << " 正在运行" << endl;
    }
    cout << name << " 退出了" << endl;
    return nullptr;
}

int main()
{
    // 创建3个线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, nullptr, waitCommand, (void *)"thread1");
    pthread_create(&tid2, nullptr, waitCommand, (void *)"thread2");
    pthread_create(&tid3, nullptr, waitCommand, (void *)"thread3");

    // 主线程来负责唤醒新线程
    while (true)
    {
        sleep(1);
        char n = '\0';
        // n代表next下一步,q代表quit退出
        cout << "请输入你的指令(n/q)";
        // cout和cin连着使用,会强制刷新缓冲区
        cin >> n;

        if (n == 'n')
        {
            pthread_cond_signal(&cond);
        }
        else
        {
            threadQuit = true;
            break;
        }
    }

    pthread_cond_broadcast(&cond);

    cout << "thread1 开始回收" << endl;
    pthread_join(tid1, nullptr);
    cout << "thread1 回收成功" << endl;
    
    cout << "thread2 开始回收" << endl;
    pthread_join(tid2, nullptr);
    cout << "thread2 回收成功" << endl;

    cout << "thread3 开始回收" << endl;
    pthread_join(tid3, nullptr);
    cout << "thread3 回收成功" << endl;


    // 销毁条件变量
    pthread_cond_destroy(&cond);

    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);
    return 0;
}

运行程序查看结果:我们发现使用了条件变量以后,唤醒线程确实是按照顺序来唤醒的。虽然第一次运行的时候是按照优先级来调度的,线程2优先级最高,线程3优先级次之,线程1优先级最低。但后来的调度都是按照这个排列顺序轮询调度的。

在这里插入图片描述

4.生产者消费者模型

生产者消费者模型是一种编程模型,它结合我们现实生活中的例子就非常好理解了。

我们生活中如果要去超市购买东西,我们就属于是消费者,消费超市里的商品。而工厂就属于生产者,生产超市里的商品。因此超市就相当于是一个集散地,他聚集了周围的用户来这里购买商品,用户不需要到处去找厂家生产自己需要的商品。他也聚集了各种各样的厂商,为厂商生产的商品提供销售的机会,厂商不需要全国各地去找消费者来消费自己生产的商品。所以生产者消费者的这个模型,可以提高效率。生产者不用到处找消费者购买,消费者也不用到处找生产者生产,既提高了生产者的生产效率,也提高了消费者的消费效率。

除此之外,由于有了超市的存在,生产厂商在生产商品的时候,不需要等待有消费者来消费了才生产,他直接将商品卖给超市即可。消费者也不需要等生产厂商生产完了才能够购买,只要超市里还有存货就可以。所以生产者消费者模型还可以大大地降低生产者和消费者之间地耦合性,生产者做自己的事情,消费者也做自己的事情,两者之间不会因为对方的动作而受影响。

在这里插入图片描述

所以在生产者消费者模型当中,生产者和消费者分别都是一批批的线程,超市就是临界资源。生产者与生产者之间是竞争关系,消费者与消费者之间也是竞争关系,所以站在线程的角度他们就是互斥的。并且生产者与消费者之间也必须是互斥的,这样才能保证读写数据不会混乱。生产者与消费者之间还必须是同步的。

在多线程编程中阻塞队列是一种常用于实现生产者和消费者模型的数据结构。阻塞队列与普通队列的区别在于如果阻塞队列为空的时候,从队列中获取数据会阻塞住;如果阻塞队列为满的时候,向队列中写入数据也会阻塞住。

在这里插入图片描述

下面我们写一个代码模拟一下创建一个生产者线程和一个消费者线程的生产者消费者模型:

BlockQueue.hpp文件:

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

using namespace std;

const uint32_t defaultCap = 5; // 阻塞队列的默认容量

template <class T>
class BlockQueue
{
public:
    BlockQueue(uint32_t capacity = defaultCap)
        : _capacity(capacity)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_conCond, nullptr);
        pthread_cond_init(&_proCond, nullptr);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_conCond);
        pthread_cond_destroy(&_proCond);
    }
    // 生产者生产接口
    void push(const T& value)
    {
        // 1.需要加锁来保护阻塞队列
        lockBlockQueue();

        // 2.判断队列是否已满,如果满了生产者就不能生产了
        while (isFull())
        {
            // 生产者等待消费者来消费商品以后被唤醒
            proBlockWait();
        }

        // 3.队列还没有满,生产者可以继续生产
        pushCore(value);

        // 4.解锁
        unlockBlockQueue();

        // 5.生产者完成生产了,需要唤醒正在等待的消费者
        conWakeUp();
    }

    // 消费者消费接口
    T& pop()
    {
        // 1.需要加锁来保护阻塞队列
        lockBlockQueue();

        // 2.判断队列是否为空,如果是空的话消费者就不能消费了
        while (isEmpty())
        {
            // 消费者等待生产者来生产商品以后被唤醒
            conBlockWait();
        }

        // 3.队列不为空,消费者可以继续消费
        T value = popCore();

        // 4.解锁
        unlockBlockQueue();

        // 5.消费者完成消费了,需要唤醒正在等待的生产者
        proWakeUp();

        // 6.返回值
        return value;
    }

private:
    // 封装加锁接口
    void lockBlockQueue()
    {
        pthread_mutex_lock(&_mutex);
    }

    // 封装解锁接口
    void unlockBlockQueue()
    {
        pthread_mutex_unlock(&_mutex);
    }

    // 判断队列是否已满
    bool isFull()
    {
        return _bq.size() == _capacity;
    }

    // 判断队列是否为空
    bool isEmpty()
    {
        return _bq.empty();
    }

    // 封装生产者等待接口
    void proBlockWait()
    {
        pthread_cond_wait(&_proCond, &_mutex);
    }

    // 封装消费者等待接口
    void conBlockWait()
    {
        pthread_cond_wait(&_conCond, &_mutex);
    }

    // 封装生产者唤醒接口
    void proWakeUp()
    {
        pthread_cond_signal(&_proCond);
    }

    // 封装消费者唤醒接口
    void conWakeUp()
    {
        pthread_cond_signal(&_conCond);
    }

    // 封装内部的入队列接口
    void pushCore(const T& value)
    {
        _bq.push(value);
    }

    // 封装内部的出队列接口
    T& popCore()
    {
        T value = _bq.front();
        _bq.pop();
        return value;
    }

private:
    queue<T> _bq;               // 阻塞队列
    uint32_t _capacity;      // 阻塞队列的容量
    pthread_mutex_t _mutex;  // 阻塞队列的互斥锁
    pthread_cond_t _conCond; // 消费者的条件变量
    pthread_cond_t _proCond; // 生产者的条件变量
};

BlockQueueTest.cc文件:

#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include "BlockQueue.hpp"

// 生产者运行函数
void* proFunction(void* args)
{
    BlockQueue<int>* bq = (BlockQueue<int>*)args;

    while(true)
    {
        // 随机生成一个0-9的数据
        int data = rand() % 10;

        // 生产数据
        bq->push(data);
        cout << "生产者生产数据完成: " << data << endl;
        sleep(2);
    }
}

// 消费者运行函数
void* conFunction(void* args)
{
    BlockQueue<int>* bq = (BlockQueue<int>*)args;

    while(true)
    {
        int data = bq->pop();
        cout << "消费者消费数据完成: " << data << endl;
    }
}

int main()
{
    // 创建一个阻塞队列
    BlockQueue<int> bq;
    
    // 创建一个随机数种子
    srand((unsigned long)time(nullptr) ^ getpid());

    // 分别创建一个生产者和消费者线程
    pthread_t producer, consumer;
    pthread_create(&producer, nullptr, proFunction, (void*)&bq);
    pthread_create(&consumer, nullptr, conFunction, (void*)&bq);

    // 回收新线程
    pthread_join(producer, nullptr);
    pthread_join(consumer, nullptr);
    return 0;
}

生产者消费者模型还体现了并发性,原因是生产者向容器里放数据和消费者从容器里取数据是互斥的,但在生产者向容器里放数据之前生产者要生产数据,在消费者从容器里取数据之后消费者要处理数据,这两个过程是并发运行的。

5.POSIX信号量

信号量本质是一个计数器,是一个用来描述临界资源数量的计数器,申请信号量就是在预定某种临界资源,当我们申请信号量成功的时候,那个对应的信号量才可以被我们唯一地使用。POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突地访问共享资源的目的。但POSIX信号量可以用于线程间同步,SystemV信号量不能用于线程间同步。

POSIX信号量的函数接口使用起来非常简单,下面分别介绍一下POSIX信号量的函数接口:

sem_init函数:
sem_init函数是信号量初始化函数,参数sem传递的是要初始化的信号量;参数pshared传递的是选项,0表示线程间共享,非零表示进程间共享;参数value表示信号量的初始值。

在这里插入图片描述

sem_destroy函数:
sem_destroy函数是信号量销毁函数,参数只需要传递信号量即可。

在这里插入图片描述

sem_wait函数:
sem_wait函数是等待信号量函数,它会将信号量的值减1,参数只需要传递信号量即可。

在这里插入图片描述

sem_post函数:
sem_post函数用于发布信号量,表示资源使用完毕了,可以归还资源了,将信号量的值加1,参数只需要传递信号量即可。

在这里插入图片描述

下面我们用信号量实现一个基于环形队列的生产者消费者模型:
我们以前设计的环形队列有两种方式,第一种是永远都是预留出一个空位不存放数据,当头指针和尾指针指向同一位置的时候就代表队列为空,当尾指针的下一个位置是头指针的时候就代表队列为满。第二种是采用计数器的方式,用来判断队列是空还是满。但今天我们用信号量的方式来设计环形队列,就不再需要采用上面这两种方式了。信号量会帮我们控制环形队列的判断工作:

  1. 当头指针和尾指针指向同一个位置的时候,要么队列为空要么队列为满,此时两个线程是互斥并且同步的。
  2. 其它任何时候,两个线程都是并发的,放数据和取数据并发运行。

那么信号量如何来保证当队列为空的时候消费线程不能消费数据,当队列为满的时候生产线程不能生产数据呢?对于生产线程,它关心的是队列空间的数量,对于消费线程,它关心的是队列数据的数量。所以我们可以设置两个信号量分别为roomSem和dataSem分别表示空间信号量和数据信号量,当生产者生产数据的时候,空间信号量减1,数据信号量加1。当消费者消费数据的时候,空间信号量加1,数据信号量减1。这样就可以保证环形队列的正常运行。

RingQueue.hpp文件:

#pragma once
#include <vector>
#include <semaphore.h>

using namespace std;

const u_int32_t cap = 10;

template <class T>
class RingQueue
{
public:
    RingQueue(int capacity = cap)
        : _ringQueue(capacity), _proIndex(0), _conIndex(0)
    {
        sem_init(&_roomSem, 0, _ringQueue.size());
        sem_init(&_dataSem, 0, 0);
    }

    ~RingQueue()
    {
        sem_destroy(&_roomSem);
        sem_destroy(&_dataSem);
    }

    // 生产者放数据到环形队列中
    void push(T& value)
    {
        // 生产者放一个数据,环形队列的空间数量就要减1
        sem_wait(&_roomSem);
        // 将数据写入环形队列的对应位置
        _ringQueue[_proIndex] = value;
        // 生产者放一个数据,环形队列的数据数量就要加1
        sem_post(&_dataSem);

        // 更新生产者写入位置的下标
        _proIndex++;
        _proIndex %= _ringQueue.size();
    }

    // 消费者从环形队列中拿数据
    T& pop()
    {
        // 消费者从环形队列中拿一个数据,数据数量要减1
        sem_wait(&_dataSem);
        // 从环形队列中拿出一个数据
        T value = _ringQueue[_conIndex];
        // 消费者从环形队列中拿一个数据,空间数量要加1
        sem_post(&_roomSem);

        // 更新消费者读取位置的下标
        _conIndex++;
        _conIndex %= _ringQueue.size();

        return value;
    }

private:
    vector<T> _ringQueue; // 环形队列
    sem_t _roomSem;       // 空间信号量,生产者关注的变量
    sem_t _dataSem;       // 数据信号量,消费者关注的变量
    u_int32_t _proIndex;  // 生产者写入的位置
    u_int32_t _conIndex;  // 消费者读取的位置
};

RingQueueTest.cc文件:

#include <iostream>
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include "RingQueue.hpp"

// 生产者运行函数
void* proFunction(void* args)
{
    RingQueue<int>* rq = (RingQueue<int>*)args;

    while(true)
    {
        sleep(1);
        int data = rand() % 10;
        rq->push(data);
        cout << "生产者放入数据成功: " << data << endl;
    }
}

void* conFunction(void* args)
{
    RingQueue<int>* rq = (RingQueue<int>*)args;

    while(true)
    {
        int data = rq->pop();
        cout << "消费者获取数据成功: " << data << endl;
    }
}

int main()
{
    // 种一个随机数种子
    srand((unsigned long)time(nullptr) & getpid());

    // 创建环形队列
    RingQueue<int> rq;

    // 创建生产线程和消费线程
    pthread_t producer, consumer;
    pthread_create(&producer, nullptr, proFunction, (void*)&rq);
    pthread_create(&consumer, nullptr, conFunction, (void*)&rq);

    // 回收新线程
    pthread_join(producer, nullptr);
    pthread_join(consumer, nullptr);
    return 0;
}

但是上面的代码其实是有一些缺陷的,上面的代码只适用于单个生产线程和单个消费线程的模型,针对多个生产线程和多个消费线程的模型并不适用,因为我们没有对_proIndex和_conIndex这些临界资源进行加锁保护,就没办法确保多个生产线程之间的互斥以及多个消费线程之间的互斥。
所以针对多生产线程和多消费线程的代码改进如下:

RingQueue.hpp文件:

#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <semaphore.h>
#include <pthread.h>

using namespace std;

const u_int32_t cap = 10;

template <class T>
class RingQueue
{
public:
    RingQueue(int capacity = cap)
        : _ringQueue(capacity), _proIndex(0), _conIndex(0)
    {
        sem_init(&_roomSem, 0, _ringQueue.size());
        sem_init(&_dataSem, 0, 0);
        pthread_mutex_init(&_proMutex, nullptr);
        pthread_mutex_init(&_conMutex, nullptr);
    }

    ~RingQueue()
    {
        sem_destroy(&_roomSem);
        sem_destroy(&_dataSem);
        pthread_mutex_destroy(&_proMutex);
        pthread_mutex_destroy(&_conMutex);
    }

    // 生产者放数据到环形队列中
    void push(T &value)
    {
        // 生产者放一个数据,环形队列的空间数量就要减1
        sem_wait(&_roomSem);
        // 对临界资源进行加锁
        pthread_mutex_lock(&_proMutex);
        // 将数据写入环形队列的对应位置
        _ringQueue[_proIndex] = value;

        // 更新生产者写入位置的下标
        _proIndex++;
        _proIndex %= _ringQueue.size();

        // 解锁
        pthread_mutex_unlock(&_proMutex);
        // 生产者放一个数据,环形队列的数据数量就要加1
        sem_post(&_dataSem);
    }

    // 消费者从环形队列中拿数据
    T &pop()
    {
        // 消费者从环形队列中拿一个数据,数据数量要减1
        sem_wait(&_dataSem);

        // 对临界资源进行加锁
        pthread_mutex_lock(&_conMutex);
        // 从环形队列中拿出一个数据
        T value = _ringQueue[_conIndex];
        // 更新消费者读取位置的下标
        _conIndex++;
        _conIndex %= _ringQueue.size();
        // 解锁
        pthread_mutex_unlock(&_conMutex);

        // 消费者从环形队列中拿一个数据,空间数量要加1
        sem_post(&_roomSem);

        return value;
    }

private:
    vector<T> _ringQueue;      // 环形队列
    sem_t _roomSem;            // 空间信号量,生产者关注的变量
    sem_t _dataSem;            // 数据信号量,消费者关注的变量
    u_int32_t _proIndex;       // 生产者写入的位置
    u_int32_t _conIndex;       // 消费者读取的位置
    pthread_mutex_t _proMutex; // 生产者的互斥锁
    pthread_mutex_t _conMutex; // 消费者的互斥锁
};

RingQueueTest.cc文件:

#include <iostream>
#include <string>
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include "RingQueue.hpp"

// 线程数据结构体
template <class T>
struct ThreadData
{
    RingQueue<T>* ringQueue;
    string name;
};

// 生产者运行函数
void* proFunction(void* args)
{
    ThreadData<int>* ptd = (ThreadData<int>*)args;
    while(true)
    {
        sleep(1);
        int data = rand() % 10;
        ptd->ringQueue->push(data);
        cout << ptd->name <<" 放入数据成功: " << data << endl;
    }
}

void* conFunction(void* args)
{
    ThreadData<int>* ctd = (ThreadData<int>*)args;

    while(true)
    {
        int data = ctd->ringQueue->pop();
        cout << ctd->name << " 获取数据成功: " << data << endl;
    }
}

int main()
{
    // 种一个随机数种子
    srand((unsigned long)time(nullptr) & getpid());

    // 创建环形队列
    RingQueue<int> rq;

    // 创建生产线程和消费线程
    pthread_t producer1, producer2, producer3;
    pthread_t consumer1, consumer2, consumer3;
    ThreadData<int> ptd1, ptd2, ptd3;
    ThreadData<int> ctd1, ctd2, ctd3;
    ptd1.ringQueue = &rq;
    ptd1.name = "producer1";
    pthread_create(&producer1, nullptr, proFunction, (void*)&ptd1);

    ptd2.ringQueue = &rq;
    ptd2.name = "producer2";
    pthread_create(&producer2, nullptr, proFunction, (void*)&ptd2);

    ptd3.ringQueue = &rq;
    ptd3.name = "producer3";
    pthread_create(&producer3, nullptr, proFunction, (void*)&ptd3);

    ctd1.ringQueue = &rq;
    ctd1.name = "consumer1";
    pthread_create(&consumer1, nullptr, conFunction, (void*)&ctd1);

    ctd2.ringQueue = &rq;
    ctd2.name = "consumer2";
    pthread_create(&consumer2, nullptr, conFunction, (void*)&ctd2);

    ctd3.ringQueue = &rq;
    ctd3.name = "consumer3";
    pthread_create(&consumer3, nullptr, conFunction, (void*)&ctd3);

    // 回收新线程
    pthread_join(producer1, nullptr);
    pthread_join(producer2, nullptr);
    pthread_join(producer3, nullptr);
    pthread_join(consumer1, nullptr);
    pthread_join(consumer2, nullptr);
    pthread_join(consumer3, nullptr);
    return 0;
}

二、线程池

有了前面线程互斥和线程同步的知识,我们现在可以设计出一个简单的线程池,在线程池中创建多个线程,然后我们可以往线程池中分放任务,让线程之间相互去竞争执行任务。

ThreadPool.hpp文件:

#pragma once
#include <iostream>
#include <memory>
#include <queue>
#include <pthread.h>
#include <unistd.h>
#include <cassert>
#include "Task.hpp"
#include "Log.hpp"

using namespace std;

int Num = 5;

template <class T>
class ThreadPool
{
public:
    ThreadPool(int threadNum = Num)
        : _threadNum(threadNum), _isStart(false)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    // 如果不加static修饰的话,是类内的成员函数,默认带第二个参数this指针
    // 这样的话线程调用就会不匹配了
    // 但是设置成static以后就不能访问类内成员了
    // 所以要传递进来this指针
    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadPool<T> *tp = (ThreadPool<T> *)args;

        while (true)
        {
            // 竞争任务,先竞争锁资源
            tp->lockQueue();
            // 判断任务队列是否有任务,如果没有任务的话,线程必须等待
            while(!tp->haveTask())
            {
                tp->waitForTask();
            }
            // 走到这里一定代表有任务了
            // 拿任务
            T task = tp->pop();
            tp->unlockQueue();

            // 在临界区外处理任务
            int one, two;
            char oper;
            task.get(&one, &two, &oper);
            Log() << "新线程完成计算任务: " << one << oper << two << "=" << task.run() << "\n";
        }
    }

    // 线程池启动接口
    void start()
    {
        assert(!_isStart);
        // 创建多个线程
        for (int i = 0; i < _threadNum; i++)
        {
            pthread_t thread;
            pthread_create(&thread, nullptr, threadRoutine, (void *)this);
        }
        _isStart = true;
    }

    // 提供给外部向任务队列中放任务
    void push(const T &in)
    {
        // 加锁
        lockQueue();
        _taskQueue.push(in);
        // 选择一个线程来执行任务
        choiceThreadForHandler();
        // 解锁
        unlockQueue();
    }

private:
    void lockQueue(){ pthread_mutex_lock(&_mutex); }
    void unlockQueue(){ pthread_mutex_unlock(&_mutex); }
    bool haveTask(){ return !_taskQueue.empty(); }
    void waitForTask(){ pthread_cond_wait(&_cond, &_mutex); }
    void choiceThreadForHandler(){ pthread_cond_signal(&_cond); }

    T& pop()
    {
        T task = _taskQueue.front();
        _taskQueue.pop();
        return task;
    }

private:
    bool _isStart;          // 标记线程池是否启动
    int _threadNum;         // 线程池中线程的数量
    queue<T> _taskQueue;    // 任务队列
    pthread_mutex_t _mutex; // 互斥锁
    pthread_cond_t _cond;   // 条件变量
};

Log.hpp文件:

#pragma once

#include <iostream>
#include <ctime>
#include <pthread.h>

std::ostream &Log()
{
    std::cout << "Fot Debug |" << " timestamp: " << (uint64_t)time(nullptr) << " | " << " Thread[" << pthread_self() << "] | ";
    return std::cout;
}

Task.hpp文件:

#pragma once

#include <iostream>
#include <string>

class Task
{
public:
    Task() : elemOne_(0), elemTwo_(0), operator_('0')
    {
    }
    Task(int one, int two, char op) : elemOne_(one), elemTwo_(two), operator_(op)
    {
    }
    int operator() ()
    {
        return run();
    }
    int run()
    {
        int result = 0;
        switch (operator_)
        {
        case '+':
            result = elemOne_ + elemTwo_;
            break;
        case '-':
            result = elemOne_ - elemTwo_;
            break;
        case '*':
            result = elemOne_ * elemTwo_;
            break;
        case '/':
        {
            if (elemTwo_ == 0)
            {
                std::cout << "div zero, abort" << std::endl;
                result = -1;
            }
            else
            {
                result = elemOne_ / elemTwo_;
            }
        }

        break;
        case '%':
        {
            if (elemTwo_ == 0)
            {
                std::cout << "mod zero, abort" << std::endl;
                result = -1;
            }
            else
            {
                result = elemOne_ % elemTwo_;
            }
        }
        break;
        default:
            std::cout << "非法操作: " << operator_ << std::endl;
            break;
        }
        return result;
    }
    int get(int *e1, int *e2, char *op)
    {
        *e1 = elemOne_;
        *e2 = elemTwo_;
        *op = operator_;
    }
private:
    int elemOne_;
    int elemTwo_;
    char operator_;
};

ThreadPoolTest.cc文件:

#include <string>
#include <ctime>
#include <cstdlib>
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Log.hpp"

int main()
{
    unique_ptr<ThreadPool<Task> > tp(new ThreadPool<Task>());
    tp->start();

    const string operators = "+-*/%";

    srand((unsigned long)time(nullptr) ^ getpid() ^ pthread_self());
    // 派发任务的线程
    while(true)
    {
        int one = rand()%50;
        int two = rand()%10;
        char oper = operators[rand()%operators.size()];
        Log() << "主线程派发计算任务: " << one << oper << two << "=?" << "\n";
        Task t(one, two, oper);
        tp->push(t);
        sleep(1);
    }
    return 0;
}

三、STL和智能指针是否线程安全

STL中的容器不是线程安全的。
原因是STL容器在设计的时候就是为了将性能挖掘到极致,而一旦涉及到了加锁保证线程安全,那么该容器的性能一定是会大大降低的。而且不同的容器加锁的方式也不同,性能可能也不同。因此STL容器不是线程安全的,如果要在多线程下使用,往往需要调用者自行保证线程安全。

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

四、其他常见的锁

  • 悲观锁: 在每次取数据时,一个线程总担心数据会被其它线程修改,所以在取数据前先加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,就会被阻塞挂起。我们之前用的互斥锁就是悲观锁。
  • 乐观锁: 乐观锁不是操作系统提供的,而是我们程序员实现的,在每次取数据的时候,一个线程总是乐观地认为数据不会被其它线程修改,因此不上锁。但是在更新数据前,这个线程会判断其它线程在数据更新前有没有对数据进行修改。主要采取的是两种方式:版本号机制和CAS操作。
  • CAS操作: 当需要更新数据时,线程会判断当前内存值和之前取得的值是否相等。如果相等则用新值更新,若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
    (乐观锁和CAS操作C++讨论的不多,一般是Java讨论的比较多)

五、读写锁问题

除了我们之前讲的生产者消费者模型是非常常见的以外,还有一种情况也是非常常见的,就是有些公共数据被修改的机会很少,相比较改写,它们被读的机会反而高很多。所以这样的数据我们在读取的时候,往往伴随着查找的操作,中间的耗时可能会比较长一些。如果给这些代码加锁了,会极大地降低效率。所以针对这种场景我们有读写锁可以解决。读写者问题与生产者消费者模型的区别是读者只读取数据并不会修改数据处理数据,而消费者会拿数据过来处理,所以读写者问题中读者与读者之间是没有任何关系的,而消费者与消费者之间是互斥的。

读写锁的接口和互斥锁、信号量的接口几乎是一样的,因为这一套锁都是POSIX标准下设计出来的,所以我们使用读写锁的成本不高。

初始化读写锁:

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);

销毁读写锁:

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

加锁和解锁:

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);// 加读锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);// 加写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);// 解锁

设置读写优先:

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
*/

下面我们可以写代码演示一下读写锁的使用:

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

using namespace std;

int gValue = 0; // 临界资源

pthread_rwlock_t rw;

void *readFunction(void *args)
{
    char *name = (char *)args;

    while (true)
    {
        sleep(1);
        pthread_rwlock_rdlock(&rw);
        cout << name << " 正在读数据: " << gValue << endl;
        pthread_rwlock_unlock(&rw);
    }
}

void *writeFunction(void *args)
{
    char *name = (char *)args;

    while (true)
    {
        pthread_rwlock_wrlock(&rw);
        gValue++;
        pthread_rwlock_unlock(&rw);
    }
}

int main()
{
    pthread_rwlock_init(&rw, nullptr);
    pthread_t reader, writer;
    pthread_create(&reader, nullptr, readFunction, (void *)"reader");
    pthread_create(&writer, nullptr, writeFunction, (void *)"writer");

    pthread_join(reader, nullptr);
    pthread_join(writer, nullptr);
    pthread_rwlock_destroy(&rw);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值