Linux系统编程:多线程(2)

目录

1.线程同步

1.1 条件变量

1.2 接口介绍

2.生产者消费者模型

2.1 模型介绍

2.2 基于BlockQueue的生产者消费者模型

2.3 POSIX信号量

2.3.1 信号量

2.3.2 信号量接口

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

3.线程池

4.线程安全的单例模式 

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

6.其他常见的各种锁

7.读者写者问

1.线程同步

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

1.1 条件变量

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

1.2 接口介绍

条件变量初始化:

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:

        cond:要初始化的条件变量

        attr:nullptr
声明一个条件变量:

pthread_cond_t cond;

条件变量销毁:
int pthread_cond_destroy(pthread_cond_t *cond);

当线程在申请共享资源时,由于条件不满足未申请到共享资源,让该线程在条件变量下等待的方法: 

等待条件满足:

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

参数:

        cond:要在这个条件变量上等待

        mutex:正在使用的互斥量,该函数会以原子性的方式将锁释放,并将自己挂起;在被                       唤醒返回的时候,会自动地获取所传入的锁

当pthread_cond_wait函数被调用的时候,会以原子性的方式,将锁释放,并将自己挂起,若不释放锁则会导致其他的进程一直处于阻塞等待锁资源的状态,这个进程在挂起后,将锁释放供其他进程使用

当pthread_cond_wait函数调用完返回的时候,会自动的重新获取你传入的锁

当条件满足后,我们需要将等待的继承唤醒,接口如下 

唤醒等待:

int pthread_cond_signal(pthread_cond_t *cond); -- 唤醒单个等待在该条件变量上的线程

int pthread_cond_broadcast(pthread_cond_t *cond);  -- 唤醒所有等待在该条件变量上的线程

该函数可以放在临界区里面,也可以放在外面

2.生产者消费者模型

2.1 模型介绍

        上图所示,我们可以将生产者看作若干个线程,消费者看作若干个线程 。仓库可以看成是缓冲区,又来让生产者线程往里存数据,消费者取数据。该模型遵从"321"原则:

  • 3种关系:生产者和生产者(互斥)、消费者和消费者(互斥)、生产者和消费者(互斥和同步)
  • 2种角色:生产者线程、消费者线程
  • 1个交易场所:一段特定结构的缓冲区

生产者消费者模型特点

  • 生产线程和消费线程解耦 
  • 支持生产者和消费者的忙闲不均问题 -- 因为存在一段缓冲区
  • 提高效率

2.2 基于BlockQueue的生产者消费者模型

        生产者往队列中放数据,消费者从队列中取数据。当队列为空时,消费者进程会变为阻塞状态,等待生产者进程往里写数据。队列满时,生产者进程变为阻塞状态,等待消费者进程从里面读走数据。

 Thread1和Thread2可以为多个线程。下面为实现代码:

代码1(BlockQueue.hpp):

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

const int gmaxCap = 5;


template <class T>
class BlockQueue
{
public:

    BlockQueue(const int &maxCap = gmaxCap)
        :_maxCap(maxCap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_consumerCond, nullptr);
        pthread_cond_init(&_productorCond, nullptr);
    }

    void Push(const T& in) // 输入型参数一般为const引用型
    {
        pthread_mutex_lock(&_mutex);
        // 细节2:充当条件判断的语法一定是while 不能是if :当线程被唤醒后,由于是while 会返回来再进行一次判断 不啊毛南族条件才会继续执行 健壮性更高
        while(is_full())
        {
            // 1.细节1 pthread_cond_wait这个函数的第二个参数 必须是我们正在使用的互斥锁
            // a.pthread_cond_wait:该函数会以原子性的方式将锁释放,并将自己挂起
            // b.pthread_cond_wait:该函数在被唤醒返回的时候 会自动重新获取你传入的锁

            // 若此时缓冲队列是满的 则无法生产 生产者进行等待
            pthread_cond_wait(&_productorCond, &_mutex);
        }
        // 走到这里一定是不满的
        _q.push(in);
        // 阻塞队列一定有数据 则可以唤醒消费者来获取数据
        // 细节3:pthread_cond_signal:该函数可以放在临界区内部 也可以放在外部
        pthread_cond_signal(&_consumerCond); // 这里可以有一定的唤醒策略
        pthread_mutex_unlock(&_mutex);
    }

    void pop(T *out) // 输出型参数一般为*型  输入输出型一般为引用型
    {
        pthread_mutex_lock(&_mutex);
        
        while(is_empty())
        {
            // 若此时缓冲队列是空的 无法获取数据 消费者阻塞等待
            pthread_cond_wait(&_consumerCond, &_mutex);
        }

        // 走到这儿保证队列数据不空
        *out = _q.front();
        _q.pop();

        // 此时 决对能保证队列里至少有一个空位 唤醒生产者来生产
        pthread_cond_signal(&_productorCond);

        pthread_mutex_unlock(&_mutex);        
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_consumerCond);
        pthread_cond_destroy(&_productorCond);
    }

private:

    bool is_empty()
    {
        return _q.empty();
    }

    bool is_full()
    {
        return _q.size() == _maxCap;
    }

private:
    std::queue<T> _q; // 封装队列
    int _maxCap; // 队列最大容量
    pthread_mutex_t _mutex; // 把队列保护起来 STL不是线程安全的
    pthread_cond_t _productorCond; // 生产者对应的条件变量
    pthread_cond_t _consumerCond; // 消费者对应的条件变量
};

代码2(MainCp.cc):

#include "BlockQueue.hpp"
#include <ctime>
#include <sys/types.h>
#include <unistd.h>

void *consumer(void *_bq)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(_bq);
    while(true)
    {
        // 消费活动
        int data;
        bq->pop(&data);
        std::cout << "消费数据:" << data << std::endl;
        sleep(1);
    }
    return nullptr;
}

void *productor(void *_bq)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(_bq);
    while(true)
    {
        // 生产活动

        int data = rand() % 10 + 1; // 1-10 先用随机数构建数据
        bq->Push(data);
        std::cout << "生产数据:" << data << "相加的任务!" <<  std::endl;
    }
    return nullptr;
}

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());
    BlockQueue<int> *bq = new BlockQueue<int>();   
    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);

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

    return 0;
}

 上述代码为生产者生产快的情况,结果如下:

 消费者消耗快的结果:

上面的代码演示了传输数据的情况,我们还可以传输任务,代码如下:

 BlockQueue.hpp与上述相同。

代码3(Task.hpp):

#pragma once

#include <iostream>
#include <functional>

class Task
{
    using func_t = std::function<int(int, int)>;
    // typedef std::function<int(int, int)> func_t;

public:
    Task()
    {}
    
    Task(int x, int y, func_t func)
        :_x(x)
        ,_y(y)
        ,_callback(func)
    {}

    int operator()()
    {
        int result = _callback(_x, _y);
        return result; 
    }

private:
    int _x;
    int _y;
    func_t _callback;
};

代码4(MainCp.cc):

#include "BlockQueue.hpp"
#include "Task.hpp"
#include <ctime>
#include <sys/types.h>
#include <unistd.h>

int myAdd(int x, int y)
{
    return x + y;
}

void *consumer(void *_bq)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task>*>(_bq);
    while(true)
    {
        // 消费活动
        int data;
        Task t;
        bq->pop(&t);
        std::cout << "消费数据:" << t() << std::endl;
        sleep(1);
    }
    return nullptr;
}

void *productor(void *_bq)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task>*>(_bq);
    while(true)
    {
        // 生产活动
        int x = rand() % 10 + 1; // 1-10 先用随机数构建数据
        int y = rand() % 5 + 1;
        Task t(x, y, myAdd);
        bq->Push(t);
        std::cout << "生产数据:" << "发送" << x << "与" << y << "相加的任务!" << std::endl;
    }
    return nullptr;
}

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());
    BlockQueue<Task> *bq = new BlockQueue<Task>();   
    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);

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

    return 0;
}

结果如下:

        根据上面的代码,可以发现:在阻塞队列中,无论有几个生产/消费者,只能由一个生产/消费者能进入队列存/取数据。那么创建多线程的生产/消费者模型的意义是什么呢?--- 多线程的生产/消费者模型是为了在获取、构建任务/处理任务时并行执行的,从而提高效率。因为获取和处理任务可能是及其耗时的。

2.3 POSIX信号量

        用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。

在上面的代码中,对临界资源的访问与操作时,有可能其时不就绪的-即临界资源不可访问。但是我们无法提前得知,只能先加锁,再检测临界资源是否可用,再决定下一步如何进行。只要我们对资源整体加锁,就默认是对资源的整体访问,此时只有一个线程可以访问这部分资源。但是,公共资源是允许多个线程同时访问不同的区域的。那么如何访问呢?

2.3.1 信号量

(1).什么是信号量?---- 本质就是一个计数器,衡量临界资源中资源数量的多少。 只要拥有信号量就一定能够拥有一部分的临界资源。

(2).申请信号量 ---- 程序员编码保证不同的线程并发的访问公共资源的不同区域,那么再访问真正的临界资源前,就可以得知公共资源的使用情况,申请成功就有资源,失败则阻塞等待资源被释放。 ---- 申请信号量的本质:对临界资源中特定小块资源的预定机制。

信号量本身也是公共资源!

申请资源 --- P操作 --- 信号量--    归还资源 --- V操作 --- 信号量++   这两个操作具有原子性。

2.3.2 信号量接口

信号量初始化

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

参数:

        sem:信号量

        pshared:是否共享,0线程共享 非0进程共享

        value:信号量的初始值

返回值:0成功  -1失败

信号量销毁:

int sem_destroy(sem_t *sem);

等待信号量:

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

发布信号量
int sem_post(sem_t *sem); // V操作 

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

        环形队列就不在赘述了,感兴趣的可以去数据结构学习。

        在环形队列中,大部分情况单生产和单消费是可以并发执行的。只有队满/空时会发生互斥同步问题。

核心工作:消费者不能超过生产者,身缠这不能超消费者一圈,生产者和消费者什么时候在一起?

信号量是用来衡量临界资源中资源数量的。

  • 对于生产者,看中队列中的剩余空间。 -- 空间资源定义一个信号量
  • 对于消费者,看重队列中的剩余数据。 -- 数据资源定义一个信号量

下面为实现改模型的代码:

代码4(RingQueue.hpp):

#pragma once 

# include <iostream>
# include <vector>
# include <cassert>
# include <semaphore.h>
# include <pthread.h>

static const int gcap = 10;

template <class T>
class RingQueue
{
private:
    void P(sem_t &sem)
    {
        int n = sem_wait(&sem);
        assert(n == 0);
        (void)n;
    }

    void V(sem_t &sem)
    {
        int n = sem_post(&sem);
        assert(n == 0);
        (void)n;
    }

public:
    RingQueue(const int &cap = gcap)
        :_queue(gcap)
        ,_cap(cap)
    {
        int n = sem_init(&_p_sapceSem, 0, _cap);
        assert(n == 0);
        n = sem_init(&_c_dataSem, 0, 0);
        assert(n == 0);
        _producttorStep = _consumerStep = 0;

        pthread_mutex_init(&_pmutex, nullptr);
        pthread_mutex_init(&_cmutex, nullptr);
    }

    void Push(const T &in)
    {
        // 加锁和申请信号量 谁先谁后比较合适
        P(_p_sapceSem); // 申请到了空间信号量 意味着一定能进行正常的生产
        pthread_mutex_lock(&_pmutex);
        _queue[_producttorStep++] = in;
        _producttorStep %= _cap;
        pthread_mutex_unlock(&_pmutex);
        V(_c_dataSem);
    }

    void Pop(T *out)
    {
        P(_c_dataSem);
        pthread_mutex_lock(&_cmutex);
        *out = _queue[_consumerStep++];
        _consumerStep %= _cap;
        pthread_mutex_unlock(&_cmutex);
        V(_p_sapceSem);
    }

    ~RingQueue()
    {
        sem_destroy(&_p_sapceSem);
        sem_destroy(&_c_dataSem);

        pthread_mutex_destroy(&_pmutex);
        pthread_mutex_destroy(&_cmutex);
    }

private:
    std::vector<T> _queue;
    int _cap; // 环形队列容量
    sem_t _p_sapceSem; // 生产者 看中的是空间资源
    sem_t _c_dataSem; // 消费者 看中的是数据资源
    int _producttorStep; // 生产者位置
    int _consumerStep; // 消费者位置

    // 为了实现多消费多生产 需要加两把锁    
    // 这里的两把锁分别对多生产者、多个消费者进行加锁,防止同时有多个生产者/消费者进入临界区 造成影响
    pthread_mutex_t _pmutex;
    pthread_mutex_t _cmutex;
};

代码5(Task.hpp): 

#pragma once

#include <iostream>
#include <functional>
#include <cstdio>
#include <string>

class Task
{
    using func_t = std::function<int(int, int, char)>;
    // typedef std::function<int(int, int)> func_t;

public:
    Task()
    {}
    
    Task(int x, int y, const char op, func_t func)
        :_x(x)
        ,_y(y)
        ,_op(op)
        ,_callback(func)
    {}

    std::string operator()()
    {
        int result = _callback(_x, _y, _op);
        char buffer[1024];
        snprintf(buffer, sizeof(buffer), "%d %c %d = %d", _x ,_op, _y, result);
        return buffer; 
    }

    std::string toTaskString()
    {
        char buffer[1024];
        snprintf(buffer, sizeof(buffer), "%d %c %d = ?", _x, _op, _y);
        return buffer; 
    }

private:
    int _x;
    int _y;
    char _op;
    func_t _callback;
};

const std::string oper = "+-*/%";

int myMath(int x, int y, char op)
{
    int result = 0;
    switch(op)
    {
        case '+':
            result = x + y;
            break;
        case '-':
            result = x - y;
            break;
        case '*':
            result = x * y;
            break;
        case '/':
        {
            if(y == 0){
                 std::cerr << "div zero error!" << std::endl;
                 result = -1;
            }
            else result = x / y;
        }
        break;
        case '%':
        {
            if(y == 0){
                 std::cerr << "mod zero error!" << std::endl;
                 result = -1;
            }
            else result = x % y;
        }
        break;
        default:
        // do nothing
            break;
    }
    return result;
}

代码6(main.cc):

# include "RingQueue.hpp"
# include "Task.hpp"
# include <pthread.h> 
# include <unistd.h>
# include <ctime>
# include <cstdlib>
# include <sys/types.h>

using namespace std;

string SelfName()
{
    char buffer[128];
    snprintf(buffer, sizeof buffer, "0x%x", pthread_self());
    return buffer;
}

void *ProductorRoutine(void *args)
{
    // RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
    while(true)
    {
        // Version1
        // sleep(2);
        // int data = rand() % 10 + 1;
        // rq->Push(data);
        // cout << "生产完成,生产的数据是:" << data << endl;
    
        // Version2
        // 模拟构建一个任务
        int x = rand() % 1000;
        int y = rand() % 1500;
        char op = oper[rand() % oper.size()];
        Task t(x, y, op, myMath);
        // 生产任务
        rq->Push(t);
        // 输出提示
        cout << SelfName() << "生产者排发了一个任务:" << t.toTaskString() << " " << endl;
        
    }
}

void *ConsumerRoutine(void *args)
{
    // RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
    
    while(true)
    {
        // verson1
        // int data;
        // rq->Pop(&data);
        // cout << "消费完成,消费的数据是:" << data << endl;
        
        // Version2
        sleep(2);
        Task t;
        // 消费任务
        rq->Pop(&t);
        string ret = t();
        cout << SelfName() <<"消费者消费了一个任务:" << ret << " " << endl;
    }
}


int main()
{
    srand((unsigned int)time(nullptr) ^ getpid() ^ pthread_self() ^ 0x71727374);
    // RingQueue<int> * rq = new RingQueue<int>();
    RingQueue<Task> * rq = new RingQueue<Task>();

    // 单生产 单消费 多生产 多消费 --》 只要保证,最终进入临界区的是一个生产者一个消费者就好了
    pthread_t c, p; // c:消费者 p:生产者

    pthread_create(&p, nullptr, ProductorRoutine, rq);
    pthread_create(&c, nullptr, ConsumerRoutine, rq);

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

    delete rq;
    return 0;
}

         上述代码创建了一个生产者一个消费者进程,生产者负责派发任务--两个数的加减乘除取模运算--,消费者负责拿到任务并得到结果,然后打印到屏幕中。

结果如下:

因为我们的代码为生产者比较快,消费者慢,所以一开始会填满队列,等待消费者消费,然后继续操作。 

3.线程池

        线程池:一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着 监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利 用,还能防止过分调度。

线程池的使用场景:

  • 需要大量的线程来完成任务,且完成任务的时间比较短。
  • 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求
  • 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用

线程池示例:

  • 创建固定数量的线程池,循环从任务队列中获取任务对象
  • 获取到任务对象后,执行任务对象的任务接口

下面为线程池的示例代码:Task.hpp与上述代码5相同、

代码7(LockGuard.hpp):

#pragma once

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

class Mutex
{
public:
    Mutex(pthread_mutex_t *lock_p = nullptr)
        :_lock_p(lock_p)
    {}

    void lock()
    {
        if(_lock_p) pthread_mutex_lock(_lock_p);
    }

    void unlock()
    {
        if(_lock_p) pthread_mutex_unlock(_lock_p);
    }

private:
    pthread_mutex_t *_lock_p;
};

class LockGuard
{
public:

    LockGuard(pthread_mutex_t *mutex)
        :_mutex(mutex)
    {
        _mutex.lock(); // 在构造函数中进行加锁
    }

    ~LockGuard()
    {
        _mutex.unlock(); // 解锁
    }

private:
    Mutex _mutex;
};

代码8(Thread.hpp):

# pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <functional>
#include <pthread.h>

namespace ThreadNs
{
    typedef std::function<void* (void*)> func_t;
    const int num = 1024;

    class Thread
    {
    private:
    
        // 在类内创建线程,想让线程执行对应的方法,需要将方法设置为static static类型的无this指针
        static void *start_routine(void *args) // 类内成员,有缺省参数 其实是有两个参数的
        {
            Thread *thread = static_cast<Thread*>(args);
            return thread->callBack();
        }

    public:
        Thread()
        {
            char namebuffer[num];
            snprintf(namebuffer, sizeof(namebuffer), "thread-%d", _threadNum++);
            _name = namebuffer;
        }

        void start(func_t func, void *args = nullptr)
        {
            _func = func;
            _args = args;
            int n = pthread_create(&_tid, nullptr, start_routine, this);  //TODO
            assert(n == 0); // 编译debug的方式发布的时候存在,release方式发布,assert就不存在了,n是一个定义但是没有使用的变量,有的编译器会警告
            (void)n;
        }
        
        void join()
        {
            int n = pthread_join(_tid, nullptr);
            assert(n == 0);
            (void)n;
        }

        void *callBack()
        {
            return _func(_args);
        }

        std::string threadname()
        {
            return _name;
        }

        ~Thread()
        {

        }

    private:
        std::string _name;
        pthread_t _tid;
        func_t _func;
        void *_args;

        static int _threadNum;
    };
    int Thread::_threadNum = 1;
}

代码9(ThreadPool.hpp):

#pragma once

# include "Thread.hpp"
# include "LockGuard.hpp"
# include <vector>
# include <queue>
# include <mutex>
# include <pthread.h>
# include <unistd.h>

using namespace ThreadNs;

const int pnum = 5;

template<class T>
class ThreadPool;

template <class T>
class ThreadData
{
public:
    ThreadPool<T> *threadpool;
    std::string name;
public:
    ThreadData(ThreadPool<T> *tp, const std::string& n)
        :threadpool(tp)
        ,name(n)
    { }
};

template<class T>
class ThreadPool
{
private:

    static void *handlerTask(void *args)
    {
        ThreadData<T> *td = static_cast<ThreadData<T>*>(args);
        while(true)
        {
            T t;
            // td->threadpool->lockQueue();
            {
                LockGuard lockguard(td->threadpool->mutex());
                while(td->threadpool->isQueueEmpty())
                {
                    td->threadpool->threadWait();
                }
                t = td->threadpool->pop(); // pop的本质 是从公共队列中拿到当前线程自己独立的栈中
            }
            // td->threadpool->unlockQueue(); 
            std::cout << td->name << " 获取了一个任务:" << t.toTaskString() << " 并处理完成,结果是:" << t() << std::endl; // 处理任务 要放在解锁之后 不然处理任务就变为串行了 而我们是要让不同进程并发的处理任务
        }
        delete td;
        return nullptr;
    }

public:

    void lockQueue() { pthread_mutex_lock(&_mutex); }
    void unlockQueue() { pthread_mutex_unlock(&_mutex); }
    bool isQueueEmpty() { return _task_queue.empty(); }
    void threadWait() { pthread_cond_wait(&_cond, &_mutex); }
    T pop()
    {
        T t = _task_queue.front();
        _task_queue.pop();
        return t;
    }

    pthread_mutex_t* mutex()
    {
        return &_mutex;
    }

    ThreadPool(const int &num = pnum)
        :_num(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        for(int i = 0; i < _num; i++)
        {
            _threads.push_back(new Thread());
        }
    }

    ThreadPool& operator=(const ThreadPool&) = delete;
    ThreadPool(const ThreadPool&) = delete;

public:
  
    void run()
    {
        for(const auto& t : _threads)
        {
            ThreadData<T> *td = new ThreadData<T>(this, t->threadname());
            t->start(handlerTask, td);
            std::cout << t->threadname() << "start..." << std::endl;
        }
    }

    void Push(const T &in)
    {
        LockGuard lockguard(&_mutex);
        // pthread_mutex_lock(&_mutex);
        _task_queue.push(in);
        pthread_cond_signal(&_cond);
        // pthread_mutex_unlock(&_mutex);
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for(const auto &t : _threads) delete t;
    }

    static ThreadPool<T>* getInstance()
    {
        // 保证线程安全
        if(nullptr == tp)
        {
            _singLock.lock();
            if(nullptr == tp)
            {
                tp = new ThreadPool<T>();
            }
            _singLock.unlock();
        }
        return tp;
    }

private:
    int _num; // 线程池中线程的数量
    std::vector<Thread*> _threads;  // 管理一批线程
    std::queue<T> _task_queue;  // 任务队列
    pthread_mutex_t _mutex; // 锁
    pthread_cond_t _cond;  // 条件变量

    static ThreadPool<T> *tp; 
    static std::mutex _singLock;
};

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

template<class T>
std::mutex ThreadPool<T>::_singLock;

代码10(main.cc):

# include "Task.hpp"
# include "ThreadPool.hpp"
# include <memory>

int main()
{
    std::unique_ptr<ThreadPool<Task>> tp(new ThreadPool<Task>());
   
    tp->run();

    int x, y;
    char op;
    while(1)
    {
        std::cout << "请输入数据1# ";
        std::cin >> x;
        std::cout << "请输入数据2# ";
        std::cin >> y;
        std::cout << "请输入将进行的运算#  ";
        std::cin >> op;

        Task t(x, y, op, myMath);
        std::cout << "刚刚录入的任务:" << t.toTaskString() << ",确认提交吗?[y/n]# " << std::endl;
        char confirm;
        std::cin >> confirm;
        if(confirm == 'y') 
        tp->Push(t);
        
        sleep(1);
    }
    return 0;
}

代码介绍:我们自己手动的输入一个=-*/%运算任务,然后将任务放入任务队列中等待线程来读取/处理任务。线程池中有_num个线程。结果如下,可以看到我们一开始在没有任务时,在线程池中创建了5个线程,输入任务后线程依次处理任务。

4.线程安全的单例模式 

单例模式是一种 "经典的, 常用的, 常考的" 设计模式。

针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是设计模式。

单例模式:一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全 局访问点,该实例被所有程序模块共享在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据。

常见的单例模式有两种:

  • 懒汉模式:如果单例对象构造十分耗时或者占用很多资源,比如加载插件、初始化网络连接、读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。所以这种情况使用懒汉模式(延迟加载)更好。
  • 饿汉模式:就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。-- 可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。

懒汉方式实现单例模式(线程安全版本):

class Singleton
{
    Singleton& operator=(const Singleton&) = delete;
    Singleton(const Singleton&) = delete;
public:
    
    static Singleton* getInstance()
    {
        if(_pInstance == nullptr) // 降低加锁时的资源浪费 提升性能
        {
            _mutex.lock();
            if(_pInstance == nullptr)
                _pInstance = new Singleton();
            _mutex.unlock();
        }
        return _pInstance;
    }

private:
    Singleton(){} // 私有化默认构造函数
    static Singleton *_pInstance;
    static Mutex _mutex;
};

Singleton* Singleton::_pInstance = nullptr;
Mutex Singleton::_mutex;

饿汉方式实现单例模式(线程安全版本):

class Singleton
{
    Singleton& operator=(const Singleton&) = delete;
    Singleton(const Singleton&) = delete;
public:
    
    static Singleton* getInstance()
    {
        return &_instance;
    }

private:
    Singleton(){} // 私有化默认构造函数
    static Singleton _instance;
};

Singleton Singleton::_instance; // 在程序入口之前就完成单例对象的初始化

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

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

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

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

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

6.其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行 锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前, 会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不 等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁,公平锁,非公平锁

自旋锁接口--与之前的锁用法完全相同:

int pthread_spin_lock(pthread_spinlock_t *lock);

int pthread_spin_trylock(pthread_spinlock_t *lock);

int pthread_spin_destroy(pthread_spinlock_t *lock);

int pthread_spin_init(pthread_spinlock_t *lock, int pshared); // psharead默认为0

int pthread_spin_lock(pthread_spinlock_t *lock);

int pthread_spin_unlock(pthread_spinlock_t *lock);

7.读者写者问题

        读者写者问题同样遵循321原则

  • 写者之间: 互斥
  • 读写之者:互斥、同步
  • 读者之间:没有关系

与生产者消费者模型的区别:消费者会拿走数据,但读者不会。

场景:一次发布,很长时间不做修改,大部分时间是被读取的,少量时间在写入

接口:

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

读者:

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

写者:

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

在任一时刻,只能有一个写者写/多个读者读。 

  • 13
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值