Linux-线程的同步与互斥

目录

[1. 线程互斥](#1. 线程互斥)

[1-1 进程线程间互斥相关背景概念](#1-1 进程线程间互斥相关背景概念)

[1-2 互斥量 mutex](#1-2 互斥量 mutex)

[1-3 互斥量实现原理探究](#1-3 互斥量实现原理探究)

[1-4 互斥量的封装](#1-4 互斥量的封装)

[2. 线程同步](#2. 线程同步)

[2-1 条件变量](#2-1 条件变量)

[2-2 同步概念与竞态条件](#2-2 同步概念与竞态条件)

[2-3 条件变量函数](#2-3 条件变量函数)

[2-4 ⽣产者消费者模型](#2-4 ⽣产者消费者模型)

[2-4-1 为何要使⽤⽣产者消费者模型](#2-4-1 为何要使⽤⽣产者消费者模型)

[2-4-2 ⽣产者消费者模型优点](#2-4-2 ⽣产者消费者模型优点)

[2-5-1 BlockingQueue](#2-5-1 BlockingQueue)

[2-5-2 C++ queue模拟阻塞队列的⽣产消费模型](#2-5-2 C++ queue模拟阻塞队列的⽣产消费模型)

[2-6 为什么 pthread_cond_wait 需要互斥量?](#2-6 为什么 pthread_cond_wait 需要互斥量?)

[2-7 条件变量使⽤规范](#2-7 条件变量使⽤规范)

[2-8 条件变量的封装](#2-8 条件变量的封装)

[2-9 POSIX信号量](#2-9 POSIX信号量)

[2-9-1 基于环形队列的⽣产消费模型](#2-9-1 基于环形队列的⽣产消费模型)

[3. 线程池](#3. 线程池)

3-1线程池设计

[3-3 线程安全的单例模式](#3-3 线程安全的单例模式)

[3-3-1 什么是单例模式](#3-3-1 什么是单例模式)

[3-3-2 单例模式的特点](#3-3-2 单例模式的特点)

[3-3-3 饿汉实现⽅式和懒汉实现⽅式](#3-3-3 饿汉实现⽅式和懒汉实现⽅式)

[3-3-4 饿汉⽅式实现单例模式](#3-3-4 饿汉⽅式实现单例模式)

[3-3-5 懒汉⽅式实现单例模式](#3-3-5 懒汉⽅式实现单例模式)

[3-3-6 懒汉⽅式实现单例模式(线程安全版本)](#3-3-6 懒汉⽅式实现单例模式(线程安全版本))

[3-4 单例式线程池](#3-4 单例式线程池)

[4. 线程安全和重⼊问题](#4. 线程安全和重⼊问题)

[5. 常⻅锁概念](#5. 常⻅锁概念)

[5-1 死锁](#5-1 死锁)

[5-2 死锁四个必要条件](#5-2 死锁四个必要条件)

5-3避免死锁

[6. STL,智能指针和线程安全](#6. STL,智能指针和线程安全)

[6-1 STL中的容器是否是线程安全的?](#6-1 STL中的容器是否是线程安全的?)

[6-2 智能指针是否是线程安全的?](#6-2 智能指针是否是线程安全的?)


1. 线程互斥

1-1 进程线程间互斥相关背景概念

临界资源:多线程执⾏流共享的资源就叫做临界资源

临界区:每个线程内部,访问临界资源的代码,就叫做临界区

互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起

保护作⽤

原⼦性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

1-2 互斥量 mutex

⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程⽆法获得这种变量。

但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

多个线程并发的操作共享变量,会带来⼀些问题。

看一段代码

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

using namespace std;
static int val = 666;

void* start_routine(void* args)
{
    string name = (const char*)args;
    while (1)
    {
        cout << name << "抢票,剩余票数:" << --val << endl;
        if(val < 0) break;
    }
    return nullptr;
}

int main()
{
    pthread_t t1,t2,t3,t4;
    pthread_create(&t1,nullptr,start_routine,(void*)"thread1");
    pthread_create(&t2,nullptr,start_routine,(void*)"thread2");
    pthread_create(&t3,nullptr,start_routine,(void*)"thread3");
    pthread_create(&t4,nullptr,start_routine,(void*)"thread4");


    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    pthread_join(t4,nullptr);

    return 0;
}

为什么可能⽆法获得争取结果?

• if 语句判断条件为真以后,代码可以并发的切换到其他线程

• usleep 这个模拟漫⻓业务的过程,在这个漫⻓的业务过程中,可能有很多个线程会进⼊该代码 段

•-- ticket 操作本⾝就不是⼀个原⼦操作

要解决以上问题,需要做到三点:

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

• 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程 进⼊该临界区。

• 如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区。 要做到这三点,本质上就是需要⼀把锁。Linux上提供的这把锁叫互斥量

互斥量的接⼝

初始化互斥量

初始化互斥量有两种⽅法:

销毁互斥量

销毁互斥量需要注意:

使⽤ PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁

• 不要销毁⼀个已经加锁的互斥量

• 已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁

互斥量加锁和解锁

调⽤ pthread_mutex_ lock 时,可能会遇到以下情况:

• 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功

• 发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到 互斥量,那么pthread_lock调⽤会陷⼊阻塞(执⾏流被挂起),等待互斥量解锁。

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

using namespace std;
//全局性的锁的初始化方式
//pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
//如何看待锁
//锁,本身就是一个共享资源!全局的变量是要被保护的,锁是用来保护全局的资源的,锁本身也是全局资源,锁的安全谁来保护呢?
//pthread_mutex_lock、pthread_mutex_unlock:加锁的过程必须是安全的!加锁的过程其实是原子的!
//如果申请成功,就继续向后执行,如果申请暂时没有成功,执行流会阻塞!


//共享资源:
static int val = 666;
//1.多个执行流进行安全访问的共享资源-临界资源
//2.我们把多个执行流中,访问临界资源的代码-临界区往往是线程代码的很小的一部分
//3,想让多个线程串行访问共享资源-互斥
//4.对一个资源进行访问的时候,要么不做,要么做完原子性(一个对资源进行的操作,如果只用一条汇编就能完成就是原子性)

//提出解决方案:加锁!

//就需要尽可能的让多个线程交叉执行
//多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换
//线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
//线程是在什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换

void* start_routine(void* args)
{
    string name = (const char*)args;

    while (1)
    {
        {
                pthread_mutex_lock(&lock);
            if (val > 0)
            {
                cout << name << "抢票,剩余票数:" << --val << endl;
                pthread_mutex_unlock(&lock);
                usleep(6666);
            }
            else
            {
                pthread_mutex_unlock(&lock);
                break;
            }
        }
    }
    return nullptr;
}

int main()
{
    pthread_t t1,t2,t3,t4;
    pthread_create(&t1,nullptr,start_routine,(void*)"thread1");
    pthread_create(&t2,nullptr,start_routine,(void*)"thread2");
    pthread_create(&t3,nullptr,start_routine,(void*)"thread3");
    pthread_create(&t4,nullptr,start_routine,(void*)"thread4");


    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    pthread_join(t4,nullptr);

    return 0;
}

1-3 互斥量实现原理探究

经过上⾯的例⼦,⼤家已经意识到单纯的 问题 i++ 或者 ++i 都不是原⼦的,有可能会有数据⼀致性

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

1-4 互斥量的封装

cpp 复制代码
//锁的封装
#include <pthread.h>

class Mutex
{
public:
    Mutex(pthread_mutex_t *mutex = nullptr):_mutex(mutex)
    {}
    void lock()
    {
        if(_mutex) pthread_mutex_lock(_mutex);
    }
    void unlock()
    {
        if (_mutex) pthread_mutex_unlock(_mutex);
    }
    ~Mutex(){}
private:
    pthread_mutex_t *_mutex;
};

//下面这个类是为了让其形成RAII型的锁
class lockGuard
{
public:
    lockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        _mutex.lock();
    }
    ~lockGuard()
    {
        _mutex.unlock();
    }
private:
    Mutex _mutex;
};

修改后的线程执行代码

2. 线程同步

2-1 条件变量

• 当⼀个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

• 例如⼀个线程访问队列时,发现队列为空,它只能等待,只到其它线程将⼀个节点添加到队列 中。这种情况就需要⽤到条件变量。

2-2 同步概念与竞态条件

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

• 竞态条件:因为时序问题,⽽导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也 不难理解

2-3 条件变量函数

初始化

销毁

等待条件满⾜

唤醒等待

我们先使⽤PTHREAD_COND/MUTEX_INITIALIZER进⾏测试,对其他细节暂不追究

• 然后将接⼝更改成为使⽤pthread_cond_init/pthread_cond_destroy的⽅式,⽅便后续进⾏封装

cpp 复制代码
//条件变量测试
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

static int val = 999;
pthread_mutex_t lock =PTHREAD_MUTEX_INITIALIZER;
//条件变量
pthread_cond_t con = PTHREAD_COND_INITIALIZER; 

void *get_val(void *args)
{
    string name = (const char*)args;
    while (1)
    {
        pthread_mutex_lock(&lock);
        pthread_cond_wait(&con,&lock);//等待
        if (val < 0)
        {
            pthread_mutex_unlock(&lock);
            break;
        }
        cout << name << "抢到票,剩余票数" << --val << endl;
        pthread_mutex_unlock(&lock);
    }
    return nullptr;
}

int main()
{
    pthread_t t1,t2,t3,t4,t5;
    pthread_create(&t1, nullptr, get_val, (void *)"thread 1");
    pthread_create(&t2, nullptr, get_val, (void *)"thread 2");
    pthread_create(&t3, nullptr, get_val, (void *)"thread 3");
    pthread_create(&t4, nullptr, get_val, (void *)"thread 4");
    pthread_create(&t5, nullptr, get_val, (void *)"thread 5");

    while (1)
    {
        //pthread_cond_signal(&con);// 唤醒一个等待在con上的线程
        pthread_cond_broadcast(&con);// 一次性唤醒所有等待在con上的线程
        cout<<"main thread singal"<<endl;
        sleep(1);
    }

    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    pthread_join(t4,nullptr);
    pthread_join(t5,nullptr);

    return 0;
}

2-4 ⽣产者消费者模型

• 321 原则(便于记忆)

2-4-1 为何要使⽤⽣产者消费者模型

⽣产者消费者模式就是通过⼀个容器来解决⽣产者和消费者的强耦合问题。⽣产者和消费者彼此之间 不直接通讯,⽽通过阻塞队列来进⾏通讯,所以⽣产者⽣产完数据之后不⽤等待消费者处理,直接扔 给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取,阻塞队列就相当于⼀个缓冲区, 平衡了⽣产者和消费者的处理能⼒。这个阻塞队列就是⽤来给⽣产者和消费者解耦的。

2-4-2 ⽣产者消费者模型优点

• 解耦 • ⽀持并发 • ⽀持忙闲不均

2-5 基于BlockingQueue的⽣产者消费者模型

2-5-1 BlockingQueue

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

2-5-2 C++ queue模拟阻塞队列的⽣产消费模型

BlockQueue.hpp

//这⾥采⽤模版,是因为队列中不仅仅可以放置内置类型,⽐如int,对象也可以作为任务来参与⽣产消费的过程哦

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

#define MAX_SIZE 5
template<typename T>
class BlockQueue
{
public:
    BlockQueue(const int &max):_max(max)
    {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_pcond,nullptr);
        pthread_cond_init(&_ccond,nullptr);
    }
    void push(const T &in)//const & 输入型
    {
        pthread_mutex_lock(&_mutex);
        //用while避免虚假唤醒
        while(this->is_full())
        {
            pthread_cond_wait(&_ccond,&_mutex);
        }

        _t.push(in);
        // 消费后唤醒生产者(有空间可生产)
        pthread_cond_signal(&_pcond);
        pthread_mutex_unlock(&_mutex);
    }
    void pop(T *out)//*输出型  //& 输入输出型
    {
        pthread_mutex_lock(&_mutex);
        //用while避免虚假唤醒
        while(this->is_empty())
        {
            pthread_cond_wait(&_pcond,&_mutex);
        }
        // 生产后唤醒消费者(有数据可消费)
        pthread_cond_signal(&_ccond);
        *out = _t.front();
         _t.pop();

        pthread_mutex_unlock(&_mutex);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }

private:
    std::queue<T> _t;//商品
    int _max;//商品最大容量
    pthread_mutex_t _mutex;
    pthread_cond_t _pcond;//生产者
    pthread_cond_t _ccond;//消费者
    bool is_full()
    {
        return _t.size() == this->_max;
    }
    bool is_empty()
    {
        return _t.empty();
    }
};

主函数:

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include "BlockQueue.hpp"
#include "Task.hpp"
#include <ctime>
#include <unistd.h>

using namespace std;
template<class P,class C>
class BlockQueues
{
public:
    BlockQueue<calTask> *c_bq;
    BlockQueue<saveTask> *s_bq;
};

const string ops = "+-*/";
int mytest(int x,int y,char op)
{
    switch (op)
    {
    case '+':
        return x + y;
        break;
    case '-':
        return x - y;
        break;
    case '*':
        return x * y;
        break;
    case '/':
        if (y == 0)
        {
            cerr << "除数不能为零" << endl;
            return -1;
        }
        else return x / y;
        break;
    default:
        cerr << "无效的操作符: " << op << endl;
        return -1; //默认分支返回错误码
        break;
    };
}

void *productor(void *_bq)
{
    BlockQueue<calTask> *bq = static_cast<BlockQueues<calTask,saveTask>*>(_bq)->c_bq;
    while(1)
    {
        int x = rand() % 100 +1;
        int y = rand() % 10 +1;
        int opCode = rand() % ops.size();

        calTask t(x,y,ops[opCode],mytest);

        bq->push(t);
        cout<<"生产者:"<<t.toTaskString()<<endl;
        sleep(1);
    }
    return nullptr;
}

void *consumer(void *_bq)
{
    BlockQueue<calTask> *bq = static_cast<BlockQueues<calTask,saveTask>*>(_bq)->c_bq;
    BlockQueue<saveTask> *save_bq = static_cast<BlockQueues<calTask,saveTask>*>(_bq)->s_bq;
    while(1)
    {
        calTask t;
        bq->pop(&t);
        string res = t();
        cout<<"消费者:"<<res<<endl;

        saveTask s(res,Save);
        save_bq->push(s);
        cout<<"消费者:存入成功"<<endl;
    }
    return nullptr;
}

void *saver(void *_bq)
{

    BlockQueue<saveTask> *save_bq = static_cast<BlockQueues<calTask,saveTask>*>(_bq)->s_bq;
    while (1)
    {
        saveTask s;
        save_bq->pop(&s);
        s(); // 回调任务写入文件
        cout << "储存者:写入成功" << endl;
    }
    return nullptr;
}


int main()
{
    //设置随机数种子(时间戳 + 进程ID)
    srand((unsigned long)time(NULL) ^ getpid());

    BlockQueues<calTask,saveTask> bq;
    bq.c_bq = new BlockQueue<calTask>(MAX_SIZE);
    bq.s_bq = new BlockQueue<saveTask>(MAX_SIZE);
  

    pthread_t c,p,s;
    pthread_create(&p,nullptr,productor,&bq);
    pthread_create(&c,nullptr,consumer,&bq);
    pthread_create(&c,nullptr,saver,&bq);

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

    delete bq.c_bq;
    delete bq.s_bq;

    return 0;
}

Task.hpp(示例任务类型)

cpp 复制代码
#include <iostream>
#include <functional>

using namespace std;

class calTask
{
    typedef std::function<int(int,int,char)>func_t;
public:
    calTask()
    {}
    calTask(int x,int y,char op,func_t func):_x(x),_y(y),_op(op),_callback(func)
    {}
    string operator()()
    {
        int res = _callback(_x,_y,_op);
        char buffer[1024];
        snprintf(buffer,sizeof(buffer),"%d %c %d = %d",_x,_op,_y,res);
        return buffer;
    }
    string toTaskString()//供生产者使用
    {
        char buffer[1024];
        snprintf(buffer,sizeof(buffer),"%d %c %d = ?",_x,_op,_y);
        return buffer;
    }
    ~calTask()
    {}
private:
    int _x;
    int _y;
    char _op;
    func_t _callback;
};

class saveTask
{
    typedef std::function<void(string &mes)>func;
public:
    saveTask()
    {}
    saveTask(const string &mes,func func):_mes(mes),_func(func)
    {}
    void operator()()
    {
        _func(_mes);
    }
    ~saveTask()
    {}
private:
    string _mes;
    func _func;
};

void Save(string &mes)
{
    const string f = "./log.txt";
    FILE *fp = fopen(f.c_str(),"a+");
    if(!fp) 
    {
        cerr<<"save error"<<endl;
        return;
    }
    fputs(mes.c_str(),fp);
    fputs("\n",fp);
    fclose(fp);
}

运行的结果是生产者不断用随机数中构建任务,传进阻塞队列中,消费者从阻塞队列中取出任务并计算,计算后还可以将任务的计算结果存储在文本文档中

2-6 为什么 pthread_cond_wait 需要互斥量?

• 条件等待是线程间同步的⼀种⼿段,如果只有⼀个线程,条件不满⾜,⼀直等下去都不会满⾜, 所以必须要有⼀个线程通过某些操作,改变共享变量,使原先不满⾜的条件变得满⾜,并且友好 的通知等待在条件变量上的线程。

• 条件不会⽆缘⽆故的突然变得满⾜了,必然会牵扯到共享数据的变化。所以⼀定要⽤互斥锁来保 护。没有互斥锁就⽆法安全的获取和修改共享数据。

2-7 条件变量使⽤规范

• 等待条件代码

给条件发送信号代码

2-8 条件变量的封装

• 基于上⾯的基本认识,我们已经知道条件变量如何使⽤,虽然细节需要后⾯再来进⾏解释,但这⾥ 可以做⼀下基本的封装,以备后⽤

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include "Lock.hpp"

namespace CondModule
{
    using namespace LockModule;

    class Cond
    {
    public:
        Cond()
        {
            int n = pthread_cond_init(&_cond, nullptr);
            (void)n; // 酌情加日志,加判断
        }

        void Wait(Mutex &mutex)
        {
            int n = pthread_cond_wait(&_cond, mutex.GetMutex());
            (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;
    };
}

2-9 POSIX信号量

POSIX信号量和SystemV信号量作⽤相同,都是⽤于同步操作,达到⽆冲突的访问共享资源⽬的。但 POSIX可以⽤于线程间同步。

初始化信号量

销毁信号量

等待信号量

发布信号量

上⼀节⽣产者-消费者的例⼦是基于queue的,其空间可以动态分配,现在基于固定⼤⼩的环形队列重写这 个程序(POSIX信号量):

2-9-1 基于环形队列的⽣产消费模型

环形队列采⽤数组模拟,⽤模运算来模拟环状特性

环形结构起始状态和结束状态都是⼀样的,不好判断为空或者为满,所以可以通过加计数器或者 标记位来判断满或者空。另外也可以预留⼀个空的位置,作为满的状态

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

// 简单封装一下信号量
class Sem
{
public:
    Sem(int n)
    {
        sem_init(&_sem, 0, n);
    }

    void P()
    {
        sem_wait(&_sem);
    }

    void V()
    {
        sem_post(&_sem);
    }

    ~Sem()
    {
        sem_destroy(&_sem);
    }

private:
    sem_t _sem;
};

环形队列:(主函数和Task.hpp与阻塞队列相似,这里不做展示)

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

using namespace std;
#define MAX_SIZE 5
template<class T>
class RingQueue
{
private:
    void P(sem_t &sem)
    {
        sem_wait(&sem);
    }
    void V(sem_t &sem)
    {
        sem_post(&sem);
    }
public:
    RingQueue(int cap = MAX_SIZE):_queue(cap),_cap(cap)
    {
        //mutex and sem init
        pthread_mutex_init(&_mutex,nullptr);
        sem_init(&_spaceSem,0,_cap);
        sem_init(&_dataSem,0,0);
        cStep = pStep = 0;
    }
    void push(const T &in)//prouctor
    {
        P(_spaceSem);
        pthread_mutex_lock(&_mutex);
        _queue[pStep++] = in;
        pStep %= _cap;
        pthread_mutex_unlock(&_mutex);
        V(_dataSem);
    }
    void pop(T *out)
    {
        P(_dataSem);
        pthread_mutex_lock(&_mutex);
        *out = _queue[cStep++];
        cStep %= _cap;
        pthread_mutex_unlock(&_mutex);
        V(_spaceSem);
    }
    ~RingQueue()
    {
        pthread_mutex_destroy(&_mutex);
        sem_destroy(&_spaceSem);
        sem_destroy(&_dataSem);
    }
private:
    int _cap;//queue size
    vector<T> _queue;
    sem_t _spaceSem;
    sem_t _dataSem;
    pthread_mutex_t _mutex;
    int pStep;
    int cStep;
};

3. 线程池

3-1线程池设计

线程池

⼀种线程使⽤模式。线程过多会带来调度开销,进⽽影响缓存局部性和整体性能。⽽线程池维护着多 个线程,等待着监督管理者分配可并发执⾏的任务。这避免了在处理短时间任务时创建与销毁线程的 代价。线程池不仅能够保证内核的充分利⽤,还能防⽌过分调度。可⽤线程数量应该取决于可⽤的并 发处理器、处理器内核、内存、⽹络sockets等的数量。

线程池的应⽤场景

• 需要⼤量的线程来完成任务,且完成任务的时间⽐较短。⽐如WEB服务器完成⽹⻚请求这样的任 务,使⽤线程池技术是⾮常合适的。因为单个任务⼩,⽽任务数量巨⼤,你可以想象⼀个热⻔⽹站 的点击次数。但对于⻓时间的任务,⽐如⼀个Telnet连接请求,线程池的优点就不明显了。因为 Telnet会话时间⽐线程的创建时间⼤多了。 比特就业课 • 对性能要求苛刻的应⽤,⽐如要求服务器迅速响应客⼾请求。 • 接受突发性的⼤量请求,但不⾄于使服务器因此产⽣⼤量线程的应⽤。突发性⼤量客⼾请求,在没 有线程池情况下,将产⽣⼤量线程,虽然理论上⼤部分操作系统线程数⽬最⼤值不是问题,短时间 内产⽣⼤量线程可能使内存到达极限,出现错误.

线程池的种类

a. 创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执⾏任务对象中 的任务接⼝ b. 浮动线程池,其他同上 此处,我们选择固定线程个数的线程池

PthreadPool.hpp

cpp 复制代码
#include "Thread.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <iostream>
#include <vector>
#include <queue>

using namespace std;
#define MAX 5

const string ops = "+-*/";
int mytest(int x,int y,char op)
{
    switch (op)
    {
    case '+':
        return x + y;
        break;
    case '-':
        return x - y;
        break;
    case '*':
        return x * y;
        break;
    case '/':
        if (y == 0)
        {
            cerr << "除数不能为零" << endl;
            return -1;
        }
        else return x / y;
        break;
    default:
        cerr << "无效的操作符: " << op << endl;
        return -1; //默认分支返回错误码
        break;
    };
}

template<class T>
class PthreadPool;

template<class T>
class ThreadData//主要用于线程运行时传递线程池与线程名
{
public:
    ThreadData(PthreadPool<T> *tp,string name):_tp(tp),_name(name)
    {}
    PthreadPool<T> *_tp;
    string _name;
};

template<class T>
class PthreadPool
{
private:
    static void* hander(void *args)//!!静态
    {
        ThreadData<T> *td = static_cast<ThreadData<T>*>(args);
        PthreadPool<T>* pool = td->_tp;
        string name = td->_name;

        while(1)
        {
            pthread_mutex_lock(&pool->_mutex);//lock,unlock,wait...这些都可以进一步封装这里不演示
            while(pool->_taskqueue.empty())
            {
                pthread_cond_wait(&pool->_cond,&pool->_mutex);
            }
            T t = pool->_taskqueue.front();
            pool->_taskqueue.pop();//删除任务
            pthread_mutex_unlock(&pool->_mutex);
            string res = t();//处理任务(不必再临界区中,会大大降低运行效率)
            cout << "[" <<name <<  "] 执行结果: " << res << endl;
        }
        return nullptr;
    }
public: 
    PthreadPool(int cap = MAX):_cap(cap)
    {
        //初始化锁与线程池
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_cond,nullptr);
        for(int i = 0;i<_cap;i++)
        {
            Thread* t = new Thread(i);
            _pool.push_back(t);//!!!
        }
    }

    void run()
    {
        for(auto &t:_pool)
        {
            //启动全部线程
            ThreadData<T> *td= new ThreadData<T>(this,t->getName());
            t->start(PthreadPool<T>::hander,td);
        }
    }
    void push(const T &in)
    {
        pthread_mutex_lock(&_mutex);
        _taskqueue.push(in);
        pthread_cond_signal(&_cond);//唤醒因任务队列空而休眠的线程
        pthread_mutex_unlock(&_mutex);      
    }

    ~PthreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for(auto &t:_pool)
        {
            t->join();
        }
    }
private:
    vector<Thread*> _pool;//线程池
    int _cap;
    queue<T> _taskqueue;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
};

线程(在上一章中有)和任务(和前面一样)已封装

3-3 线程安全的单例模式

3-3-1 什么是单例模式

单例模式(Singleton Pattern)是一种创建型设计模式 ,它的核心目标是:保证一个类在整个程序运行期间,只有唯一的一个实例对象,并且提供一个全局访问点来获取这个实例

3-3-2 单例模式的特点

某些类,只应该具有⼀个对象(实例),就称之为单例. 例如⼀个男⼈只能有⼀个媳妇. 在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中.此时往往要⽤⼀个单例 的类来管理这些数据.

3-3-3 饿汉实现⽅式和懒汉实现⽅式

3-3-4 饿汉⽅式实现单例模式

cpp 复制代码
 template <typename T>
 class Singleton {
 static T data;
 public:
 static T* GetInstance() {
 return &data;
 }
 }

只要通过Singleton这个包装类来使⽤T对象,则⼀个进程中只有⼀个T对象的实例.

3-3-5 懒汉⽅式实现单例模式

cpp 复制代码
template <typename T>
class Singleton {
    static T* inst;

public:
    static T* GetInstance() {
        if (inst == NULL) {
            inst = new T();
        }
        return inst;
    }
};

存在⼀个严重的问题,线程不安全. 第⼀次调⽤GetInstance的时候,如果两个线程同时调⽤,可能会创建出两份T对象的实例. 但是后续再次调⽤,就没有问题了.

3-3-6 懒汉⽅式实现单例模式(线程安全版本)

cpp 复制代码
// 懒汉模式,线程安全
template <typename T>
class Singleton {
    volatile static T* inst;  // 需要设置 volatile 关键字,否则可能被编译器优化.
    static std::mutex lock;

public:
    static T* GetInstance() {
        if (inst == NULL) {  // 双重判定空指针,降低锁冲突的概率,提高性能.
            lock.lock();     // 使用互斥锁,保证多线程情况下也只调用一次 new.
            if (inst == NULL) {
                inst = new T();
            }
            lock.unlock();
        }
        return inst;
    }
};

注意事项: 1. 加锁解锁的位置 2. 双重if判定,避免不必要的锁竞争 3. volatile关键字防⽌过度优化

3-4 单例式线程池

cpp 复制代码
#include "Thread.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <iostream>
#include <vector>
#include <queue>

using namespace std;
#define MAX 5

const string ops = "+-*/";
int mytest(int x,int y,char op)
{
    switch (op)
    {
    case '+':
        return x + y;
        break;
    case '-':
        return x - y;
        break;
    case '*':
        return x * y;
        break;
    case '/':
        if (y == 0)
        {
            cerr << "除数不能为零" << endl;
            return -1;
        }
        else return x / y;
        break;
    default:
        cerr << "无效的操作符: " << op << endl;
        return -1; //默认分支返回错误码
        break;
    };
}

template<class T>
class PthreadPool;

template<class T>
class ThreadData//主要用于线程运行时传递线程池与线程名
{
public:
    ThreadData(PthreadPool<T> *tp,string name):_tp(tp),_name(name)
    {}
    PthreadPool<T> *_tp;
    string _name;
};

template<class T>
class PthreadPool
{
private:
    static void* hander(void *args)//!!静态
    {
        ThreadData<T> *td = static_cast<ThreadData<T>*>(args);
        PthreadPool<T>* pool = td->_tp;
        string name = td->_name;

        while(1)
        {
            pthread_mutex_lock(&pool->_mutex);//lock,unlock,wait...这些都可以进一步封装这里不演示
            while(pool->_taskqueue.empty())
            {
                pthread_cond_wait(&pool->_cond,&pool->_mutex);
            }
            T t = pool->_taskqueue.front();
            pool->_taskqueue.pop();//删除任务
            pthread_mutex_unlock(&pool->_mutex);
            string res = t();//处理任务(不必再临界区中,会大大降低运行效率)
            cout << "[" <<name <<  "] 执行结果: " << res << endl;
        }
        return nullptr;
    }
//public: 
//懒汉模式:(是"延时加载"即把构造函数私有化,在真正使用时在创建)
    PthreadPool(int cap = MAX):_cap(cap)
    {
        //初始化锁与线程池
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_cond,nullptr);
        for(int i = 0;i<_cap;i++)
        {
            Thread* t = new Thread(i);
            _pool.push_back(t);//!!!
        }
    }

    public:
    //懒汉模式创建线程池
    static PthreadPool<T>* getInstance() {

 // 

        if (nullptr == threadpool) {
            pthread_mutex_lock(&signalmutex);
            if (nullptr == threadpool) {
                threadpool = new PthreadPool<T>();
            }
            pthread_mutex_unlock(&signalmutex);
        }
        return threadpool;
    }
    void run()
    {
        for(auto &t:_pool)
        {
            //启动全部线程
            ThreadData<T> *td= new ThreadData<T>(this,t->getName());
            t->start(PthreadPool<T>::hander,td);
        }
    }
    void push(const T &in)
    {
        pthread_mutex_lock(&_mutex);
        _taskqueue.push(in);
        pthread_cond_signal(&_cond);//唤醒因任务队列空而休眠的线程
        pthread_mutex_unlock(&_mutex);      
    }

    ~PthreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for(auto &t:_pool)
        {
            t->join();
        }
    }
private:
    vector<Thread*> _pool;//线程池
    int _cap;
    queue<T> _taskqueue;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;

//单例模式添加的
    static PthreadPool<T> *threadpool;
    static pthread_mutex_t signalmutex;
};
template<class T>
PthreadPool<T> * PthreadPool<T>::threadpool = nullptr;
template<class T>
pthread_mutex_t PthreadPool<T>::signalmutex = PTHREAD_MUTEX_INITIALIZER;

解释一下为什么单例模式那里要有两次判断

4. 线程安全和重⼊问题

概念 线程安全:就是多个线程在访问共享资源时,能够正确地执⾏,不会相互⼲扰或破坏彼此的执⾏结 果。⼀般⽽⾔,多个线程并发同⼀段只有局部变量的代码时,不会出现不同的结果。但是对全局变量 或者静态变量进⾏操作,并且没有锁保护的情况下,容易出现该问题。

重⼊:同⼀个函数被不同的执⾏流调⽤,当前⼀个流程还没有执⾏完,就有其他的执⾏流再次进⼊, 我们称之为重⼊。⼀个函数在重⼊的情况下,运⾏结果不会出现任何不同或者任何问题,则该函数被 称为可重⼊函数,否则,是不可重⼊函数。 学到现在,其实我们已经能理解重⼊其实可以分为两种情况:• 多线程重⼊函数 • 信号导致⼀个执⾏流重复进⼊函数

5. 常⻅锁概念

5-1 死锁

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

• 为了⽅便表述,假设现在线程A,线程B必须同时持有锁1和锁2,才能进⾏后续资源的访问

5-2 死锁四个必要条件

• 互斥条件:⼀个资源每次只能被⼀个执⾏流使⽤ ◦ 好理解,不做解释

• 请求与保持条件:⼀个执⾏流因请求资源⽽阻塞时,对已获得的资源保持不放

• 不剥夺条件:⼀个执⾏流已获得的资源,在末使⽤完之前,不能强⾏剥夺

• 循环等待条件:若⼲执⾏流之间形成⼀种头尾相接的循环等待资源的关系

5-3避免死锁

这里不做演示

6. STL,智能指针和线程安全

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

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

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

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

相关推荐
提伯斯6462 小时前
Orangepi R1内置了哪些网卡驱动?(全志H3的板子)
linux·网络·wifi·全志h3
技术摆渡人2 小时前
专题二:【驱动进阶】打破 Linux 驱动开发的黑盒:从 GPIO 模拟到 DMA 陷阱全书
android·linux·驱动开发
wishchin2 小时前
Jetson Orin Trt: No CMAKE_CUDA_COMPILER could be found
linux·运维·深度学习
ArrebolJiuZhou2 小时前
03 rtp,rtcp,sdp的包结构
linux·运维·服务器·网络·arm开发
403240733 小时前
Ubuntu/Jetson 通用:NVMe 硬盘分区、挂载及开机自动挂载完整教程
linux·运维·ubuntu
田地和代码3 小时前
linux应用用户安装jdk以后 如果root安装hbase客户端需要jdk还需要再次安装吗
java·linux·hbase
乔碧萝成都分萝3 小时前
二十四、Linux如何处理中断
linux·驱动开发·嵌入式
输出输入3 小时前
那鸿蒙应用的后端服务器用什么语言编写
服务器·华为
真的想上岸啊3 小时前
2、刷机+mobaxterm登录
linux