linux线程(3)——线程池、线程安全与可重入与死锁

目录

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

1.POSIX信号量

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

二、读者写者问题

三、线程池

1.线程池        

四、线程安全的单例模式

五、线程安全与可重入

1.线程安全

①不保护全局变量的函数。

②保持跨越多个调用的状态的函数

③返回指向静态变量的指针的函数

④调用线程不安全的函数的函数

2.可重入

3.使用可重入的库函数

六、死锁

1.  死锁

2.死锁四个必要条件

3.避免死锁

4.避免死锁的算法

①银行家算法

②死锁检测算法


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

1.POSIX信号量

        信号量是一个计数器,描述临界资源数量的计数器。

        通常操作为++,--,这两个操作都是原子的。

        加加代表归还资源,减减代表申请资源。

        信号量你一旦申请成功,就会获得指定资源。

        接上篇文章,我们谈到基于阻塞队列的生产者消费者模型,其中为了防止一个线程恶意竞争锁而导致其他线程饥饿,我们采用了线程同步的方式来解决问题。这次了解到了信号量我们是否也可以用它来解决上面的问题。

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

        关于生产者和消费者访问同一个位置时,会出现两种情况:

        为空:生产者先运行,消费者待命,等待有资源可以消费。

        为满:消费者先运行,生产者待命,等待有空间可以生产。

        这两步骤的运行都是由信号量来保证。

代码:

ringqueue.hpp:

#include <iostream>
#include <vector>
#include <ctime>
#include <unistd.h>
#include <semaphore.h>
using namespace std;

const int size = 8;

template <class T>
class Ringqueue
{
public:
    Ringqueue(int cap = size)
        : _ringqueue(cap), _proindex(0), _conindex(0)
    {
        // 初始化信号量
        sem_init(&_dataspace, 0, _ringqueue.size());
        sem_init(&_datanum, 0, 0);
    }

    void produce(const T &in)
    {
        // 等待信号量,如果有就会使该信号量减减继续如果没有就阻塞
        sem_wait(&_dataspace);
        _ringqueue[_proindex] = in;

        // 增加信号量
        sem_post(&_datanum);

        _proindex++;
        _proindex %= _ringqueue.size();
    }
    T consume()
    {
        sem_wait(&_datanum);
        T out = _ringqueue[_conindex];

        sem_post(&_dataspace);

        _conindex++;
        _conindex %= _ringqueue.size();

        return out;
    }
    ~Ringqueue()
    {
        // 销毁信号量
        sem_destroy(&_dataspace);
        sem_destroy(&_datanum);
    }

private:
    // queue
    vector<T> _ringqueue;
    // 信号量
    // 空间大小
    sem_t _dataspace;
    // 数据个数
    sem_t _datanum;
    // 生产者生产的下标
    uint32_t _proindex;
    // 消费者消费的下标
    uint32_t _conindex;
};
ringqueue.cc:

#include "ringqueue.hpp"

void *productor(void *args)
{
    Ringqueue<int> *rqp = static_cast<Ringqueue<int> *>(args);
    while (1)
    {
        sleep(1);
        int data = rand() % 199;
        rqp->produce(data);
        cout << "Time : " << (unsigned long)time(nullptr) << ' ' << "thread : " << pthread_self()
             << ' ' << "produce data success,data is : " << data << endl;
    }
}

void *concumer(void *args)
{
    Ringqueue<int> *rqp = static_cast<Ringqueue<int> *>(args);
    while (1)
    {
        int out = rqp->consume();
        cout << "Time : " << (unsigned long)time(nullptr) << ' ' << "thread : " << pthread_self()
             << ' ' << "consume data success,data is : " << out << endl;
    }
}

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());
    pthread_t con;
    pthread_t pro;

    Ringqueue<int> rq;
    pthread_create(&con, nullptr, concumer, &rq);
    pthread_create(&pro, nullptr, productor, &rq);

    pthread_join(con, nullptr);
    pthread_join(pro, nullptr);
    return 0;
}

  结果:

 多线程:

        只需要,在申请信号量时加上锁。

 结果:

二、读者写者问题

        在编写多线程时也会有这样一种模型,在这个模型中读的次数要比改写的次数要远远高于,并且在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地 降低我们程序的效率。所以要专门来提供方案来解决这类问题,方案就是读写锁。

        解决问题之前,我们首先要了解读者写者的关系。

        写者与写者:互斥关系

        写者与读者:互斥关系

        读者与读者:并发关系

        为什么消费者与消费者之间的关系就是互斥,这里读者与读者可以并发,本质区别就是这是读并没有修改临界资源中的数据。

        既然这样那就要针对处理:

        读者-读锁   写者-写锁  

        我们来见识下读写锁的销毁与初始化:

        读锁:

        写锁:        

         解锁:

代码:

int count = 0;
pthread_rwlock_t rwmutex;
void *read(void *argc)
{
    char *msg = static_cast<char *>(argc);
    sleep(2);
    while (1)
    {
        pthread_rwlock_rdlock(&rwmutex);
        cout<<"thread : "<<pthread_self()<<" reader read : "<<count<<endl;
        pthread_rwlock_unlock(&rwmutex);
        sleep(1);
    }
}
void *write(void *argc)
{
    char *msg = static_cast<char *>(argc);
    while (1)
    {
        sleep(1);
        pthread_rwlock_wrlock(&rwmutex);
        count++;
        cout<<"writer write : count++"<<endl;
        pthread_rwlock_unlock(&rwmutex);
    }
}

int main()
{

    pthread_t r,r1,r2,r3,r4,r5,w;
    pthread_create(&r, nullptr, read, (void *)"reader");
    pthread_create(&r1, nullptr, read, (void *)"reader");
    pthread_create(&r2, nullptr, read, (void *)"reader");
    pthread_create(&r3, nullptr, read, (void *)"reader");
    pthread_create(&r4, nullptr, read, (void *)"reader");
    pthread_create(&r5, nullptr, read, (void *)"reader");
    pthread_create(&w, nullptr, write, (void *)"writer");

    pthread_join(r, nullptr);
    pthread_join(r1, nullptr);
    pthread_join(r2, nullptr);
    pthread_join(r3, nullptr);
    pthread_join(r4, nullptr);
    pthread_join(r5, nullptr);
    pthread_join(w, nullptr);

    pthread_rwlock_destroy(&rwmutex);

    return 0;
}

结果:

三、线程池

1.线程池        

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

        我们来写写一个具有固定数量线程暴露出调用接口的线程池。

threadpool.hpp:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <queue>
#include <assert.h>
#include <cstdlib>
#include <memory>
#include <ctime>
#include <string>
#include "Task.hpp"

using namespace std;

const int gthreadnum = 8;

template <class T>
class Threadpool
{
public:
    Threadpool(int num = gthreadnum) : _isStart(false), _threadnum(num)
    {
        assert(_threadnum > 0);
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }
    ~Threadpool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }
    static void *fuc(void *argc)
    {
        pthread_detach(pthread_self());
        Threadpool<T> *tp = static_cast<Threadpool<T> *>(argc);
        while (1)
        {
            tp->lockqueue();
            while (tp->isempty())
            {
                tp->waittask();
            }
            T t = tp->pop();
            tp->unlockqueue();
            int result = t.calculate();
            int one, two;
            char opr;
            t.getelem(&one, &two, &opr);
            cout << "Time : " << (unsigned long)time(nullptr) << ' ' << "thread : " << pthread_self()
                 << ' ' << "do task success,data is : " << one << ' ' << opr << ' ' << two << " = " << result << endl;
        }
    }
    void start()
    {
        assert(!_isStart);
        for (int i = 0; i < _threadnum; i++)
        {
            pthread_t th1;
            pthread_create(&th1, nullptr, fuc, this);
        }
        _isStart = true;
    }
    void push(const T &in)
    {
        lockqueue();
        _taskqueue.push(in);
        choicethread();
        unlockqueue();
    }

private:
    void lockqueue()
    {
        pthread_mutex_lock(&_mutex);
    }
    void unlockqueue()
    {
        pthread_mutex_unlock(&_mutex);
    }
    bool isempty()
    {
        return _taskqueue.empty();
    }
    void waittask()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }
    void choicethread()
    {
        pthread_cond_signal(&_cond);
    }
    T pop()
    {
        T temp = _taskqueue.front();
        _taskqueue.pop();
        return temp;
    }

private:
    bool _isStart;
    uint32_t _threadnum;
    queue<T> _taskqueue;
    // 让线程互斥的获取任务队列的任务
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
};
threadpool.cc:
#include "threadpool.hpp"
#include "Task.hpp"

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

int main()
{
    unique_ptr<Threadpool<Task>> utp(new Threadpool<Task>());
    utp->start();

    srand((unsigned long)time(nullptr) ^ getpid());
    while (1)
    {
        int one = rand() % 3212;
        int two = rand() % 131;
        char opr = opera[rand()%opera.size()];
        cout << "Time : " << (unsigned long)time(nullptr) << ' ' << "thread : " << pthread_self() << ' ' << "produce task success,data is : " << one << ' ' << opr << ' ' << two << " -> ?" << endl;
        Task t(one, two, opr);
        utp->push(t);
        sleep(1);
    }
}

结果:

        至于Task.hpp,可以去前几篇文章寻找一下。限于篇幅就不重复敲了。 

四、线程安全的单例模式

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

        把上面的线程池改为单例模式

private:
    Threadpool(int num = gthreadnum) : _isStart(false), _threadnum(num)
    {
        assert(_threadnum > 0);
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }
    Threadpool(const Threadpool<T> &) = delete;
    void operator=(Threadpool<T> &) = delete;
public:
    static Threadpool<T> *getinstance()
    {
        static pthread_mutex_t mutex;
        if (instance == nullptr)
        {
            pthread_mutex_lock(&mutex);
            if (instance == nullptr)
            {
                instance = new Threadpool<T>();
            }
            pthread_mutex_unlock(&mutex);
        }
        assert(instance != nullptr);
        return instance;
    }
private:
    bool _isStart;
    uint32_t _threadnum;
    queue<T> _taskqueue;
    // 让线程互斥的获取任务队列的任务
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;

    static Threadpool<T> *instance;
};

template <class T>
Threadpool<T> *Threadpool<T>::instance = nullptr;

结果:

        虽然没有使用多个线程去调用getinstance(),但是这在多线程版本也是可行的。 

五、线程安全与可重入

1.线程安全

        一个函数被称为线程安全的,当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果。如果一个函数不是线程安全的,我们就说它是线程不安全的。

四类线程不安全函数:

①不保护全局变量的函数。

        在函数内部为临界区加上锁,就可以将它变为线程安全函数,这样做的缺点是增加程序运行时间。

②保持跨越多个调用的状态的函数

        rand函数就是线程不安全的,因为当前调用的结果依赖于前次调用的中间结果。如果,我们为srand置一个种子,一个线程调用rand会得到,预期的随机数。如果多个线程同时调用rand函数,反而有可能得到相同的数字。因为,rand函数得出的伪随机数依赖于srand所给出的值,多线程调用rand函数,而同一时间srand函数,只给出一个值,多个线程拿到的值一样,造就随机值都是一样的。使得像这样的rand函数线程安全唯一的方式就是重写它,使得它不再使用任何static数据,而是依靠调用者在参数中传递状态信息。

③返回指向静态变量的指针的函数

        某些函数,例如ctime和gethostbyname,将计算结果放在static变量中,然后返回一个指向这个变量的指针。如果我们从并发线程中调用这些函数,那么将可能发生灾难,因为正在被一个线程调用的结果会被另一个线程悄悄地覆盖了。解决方案两种方法:一种重写函数,调用者传递存放结果的地址。另一种选择就是使用加锁-复制技术。基本思想是将线程不安全函数与互斥锁联系起来。在每次调用线程不安全函数时,对它加锁,再把返回结果复制到一个私有的内存位置,然后解锁。

④调用线程不安全的函数的函数

        调用第二类线程不安全的函数,这种依赖于跨越多次调用的状态的函数,会导致调用该函数的函数线程不安全。

2.可重入

        同一个函数被不同执行流调用,当前一个流程还没有执行完,就有其他执行流再次进入,我们称之为重入。一个函数在重入的情况下,运算结果不会出现任何问题,则该函数被称为可重入函数,反之被称为不可重入函数。它有一个最重要的一个特性是:当它们被多个线程调用时,不会引用任何共享的数据。

        线程安全和可重入两个概念很容易搞混,它们是这样的关系:

         如果所有的函数参数都是传值传递的(既没有指针),并且所有的数据引用的都是本地的自动栈变量(既没有引用静态或全局变量),那么函数就是显式可重入的。

        如果允许显式可重入函数中一些参数是引用传递(传递指针),那么就得到了一个隐式可重入函数,前提是这个指针指向非共享数据。

3.使用可重入的库函数

        大多数Linux函数,包括定义在标准C库中的函数(例如:malloc、free、relloc、printf和scanf)都是线程安全的。小部分例外,但这小部分例外,Linux系统提供大多数线程不安全函数的可重入版本。可重入版本的名字总是以“_r”结尾。

        像我们先前提到过的rand函数就有可重入版本rand_r。

六、死锁

1.  死锁

        死锁是指一组进程中不同的进程占用不会释放的资源,但因互相去申请其他进程不会释放的资源而处于永久等待的状态。

比如:

void *fuc(void *argc)
{
    while (1)
    {
        pthread_mutex_lock(&mutexA);
        pthread_mutex_lock(&mutexB);

        cout << "我是线程1"
             << "mythread id :" << pthread_self() << endl;

        pthread_mutex_unlock(&mutexA);
        pthread_mutex_unlock(&mutexB);
    }
}

void *fuc1(void *argc)
{
    while (1)
    {
        pthread_mutex_lock(&mutexA);
        pthread_mutex_lock(&mutexB);

        cout << "我是线程2"
             << "mythread id :" << pthread_self() << endl;

        pthread_mutex_unlock(&mutexA);
        pthread_mutex_unlock(&mutexB);
    }
}

int main()
{
    pthread_t td1, td2;
    pthread_create(&td1, nullptr, fuc, NULL);
    pthread_create(&td1, nullptr, fuc1, NULL);

    pthread_join(td1, NULL);
    pthread_join(td2, NULL);

    pthread_mutex_destroy(&mutexA);
    pthread_mutex_destroy(&mutexB);

    

        这个代码跑起来并没有什么问题

        但如果,线程1先去申请mutexA,同时线程2去申请mutexB,然后线程1要去申请mutexB,线程2要去申请mutexA,这就会出现问题。

void *fuc(void *argc)
{
    while (1)
    {
        pthread_mutex_lock(&mutexA);
        sleep(1);
        pthread_mutex_lock(&mutexB);
        ...


void *fuc1(void *argc)
{
    while (1)
    {
        pthread_mutex_lock(&mutexB);
        sleep(1);
        pthread_mutex_lock(&mutexA);
        ...

        进程卡住了,两个线程互相去申请对方占用的锁,申请不到便会挂起。这就是死锁现象。

2.死锁四个必要条件

①互斥条件: 线程访问临界区是互斥的。一个资源每次智能被一个执行流使用。

②请求与保持条件: 一个执行流因申请资源而阻塞,而自己的申请到的资源未释放。

③不剥夺条件: 一个线程在未使用完资源时,不能强行剥夺。

④循环等待条件:若干个执行流都在等待对方的资源,而形成首尾相接的循环等待资源的关系。    

3.避免死锁

①破坏四个死锁必要条件。

②加锁顺序一致。

③避免锁未释放的场景。

④资源一次性释放。

4.避免死锁的算法

①银行家算法

        每一个新进程在执行之前,必须先声明自己需要的每种资源的最大单元个数。当进程申请某一部分资源的时候,os会判断是否可以分配,如果可以分配并且os在分配之后处于安全状态,才会将资源分配给进程,反之则不会。

②死锁检测算法

        死锁会导致一个现象,首尾相接的互相申请的线程阻塞在一起,所以这个算法就是检查这种现象的。

        我们在给线程加锁时不一定非要用pthread_mutex_lock(),也可以用pthread_mutex_trylock() 

        lock家族的成员,它是非阻塞式申请mutex,如果申请不到则返回,一般会尝试while循环调用,如果没成功可以在循环体内检测某些必要条件是否满足,避免死锁现象。

        感谢观看,如有问题请指出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值