Linux线程篇——信号量与线程池(三)


一、POSIX信号量

1.1 相关概念和接口

信号量本质是一个计数器,用于描述临界资源的大小。将临界资源分成不同份,让多个线程实现并发执行,这也是多线程预定资源的手段。

每个线程想要访问临界资源,都先得申请到信号量资源,只要申请到,都一定会有属于该线程的一份资源。

初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值

销毁信号量

int sem_destroy(sem_t *sem);

等待信号量

功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()

发布信号量

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1int sem_post(sem_t *sem);//V()

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

环形队列在数据结构里面已经涉及到了。由于环形队列(n个空间)的状态有n+1种无法用n个空间表示,一般有两种解决办法:

  1. 使用一个额外变量充当计数器,表示当前的状态
  2. 少使用一个空间,这样就只有n种状态了

而我们的信号量就可以充当这样的计数器。

ring_cp.cc

#include "RingQueue.hpp"

using namespace ns_ring_queue;
#include <unistd.h>
#include <time.h>

void* producter(void* args)
{
    RingQueue<int>* rq = (RingQueue<int>*)args;
    while (true)
    {
        int t = rand()%10 + 1;
        rq->Push(t);
        std::cout << "producter send task: " << t << std::endl;
    }
}

void* consumer(void* args)
{
    RingQueue<int>* rq = (RingQueue<int>*)args;
    while (true)
    {
        int c = 0;
        rq->Pop(&c);
        std::cout << "线程开始消费" << c << std::endl;
        sleep(1);
    }
}
int main()
{
    srand((unsigned long)time(nullptr));
    const int N = 5;
    pthread_t master;
    pthread_t worker[N];

    RingQueue<int> *rq = new RingQueue<int>;

    pthread_create(&master, nullptr, producter, (void*)rq);
    for (int i = 0; i < N; i ++ )
        pthread_create(&worker[i], nullptr, consumer, (void*)rq);
    
    pthread_join(master,nullptr);
    for (int i = 0; i < N; i ++ )
        pthread_join(worker[i], nullptr);
    
    return 0;
}

RingQueue.hpp

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

namespace ns_ring_queue
{
    const int g_default_cap = 5;

    template <class T>
    class RingQueue
    {
    private:
        std::vector<T> ring_queue_;
        int cap_; // 环形队列的大小

        sem_t blank_sem_; // 生产者只关心空位置的数量
        sem_t data_sem_;  // 消费者只关心数据的数量

        int c_step_; // 消费者当前消费的位置
        int p_step_; // 生产者生产存放的下标

        // 互斥量,用于同类型直接的互斥加锁
        pthread_mutex_t c_mtx_;
        pthread_mutex_t p_mtx_;

    public:
        RingQueue(int cap = g_default_cap) : cap_(cap), ring_queue_(cap)
        {
            sem_init(&blank_sem_, 0, cap);
            sem_init(&data_sem_, 0, 0);

            c_step_ = p_step_ = 0;

            pthread_mutex_init(&p_mtx_, nullptr);
            pthread_mutex_init(&c_mtx_, nullptr);
        }

        ~RingQueue()
        {
            pthread_mutex_destroy(&p_mtx_);
            pthread_mutex_destroy(&c_mtx_);

            sem_destroy(&blank_sem_);
            sem_destroy(&data_sem_);
        }

    public:
        void Push(const T &in)
        {
            // 等待信号量,信号量减1
            sem_wait(&blank_sem_); // P()

            // 同类互斥,加锁
            pthread_mutex_lock(&p_mtx_);
            // 空位置生产
            ring_queue_[p_step_] = in;
            p_step_++;
            p_step_ %= cap_;
            pthread_mutex_unlock(&p_mtx_);

            // 发布信号量,信号量加1
            sem_post(&data_sem_); // V()
        }

        void Pop(T *out)
        {
            // 等待信号量,信号量减1
            sem_wait(&data_sem_); // P()

            // 同类互斥,加锁
            pthread_mutex_lock(&c_mtx_);
            // 空位置生产
            *out = ring_queue_[c_step_];
            c_step_++;
            c_step_ %= cap_;
            pthread_mutex_unlock(&c_mtx_);

            // 发布信号量,信号量加1
            sem_post(&blank_sem_); // V()
        }
    };
}

在这里插入图片描述

二、线程池

2.1 概念

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

总的来说为了提高效率,提前准备好线程(等到用的时候再申请比较慢,类比内存池),用来随时处理任务。

线程池的应用场景:

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.

线程池的种类:

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

2.2 代码实现

main.cc

#include "thread_pool.hpp"

using namespace ns_threadpool;

int main()
{
    ThreadPool<int> *tp = new ThreadPool<int>;


    while (true)
    {
        sleep(1);
        int t = rand()%10 + 1;
        std::cout << "send task: " << t << std::endl;
        tp->PushTask(t);
    }
}

thread_pool.hpp

#pragma once

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

namespace ns_threadpool
{
    const int g_num = 5;
    template <class T>
    class ThreadPool
    {
    private:
        std::queue<T> task_queue_;
        int num_; // 线程池线程的数量

        pthread_mutex_t mtx_;
        pthread_cond_t cond_;

    private:
        void Lock()
        {
            pthread_mutex_lock(&mtx_);
        }
        void Unlock()
        {
            pthread_mutex_unlock(&mtx_);
        }

        void Wait()
        {
            pthread_cond_wait(&cond_, &mtx_);
        }
        void Wakeup()
        {
            pthread_cond_signal(&cond_);
        }
        bool IsEmpty()
        {
            return task_queue_.empty();
        }

    public:
        ThreadPool(int num = g_num) : num_(num)
        {
            pthread_mutex_init(&mtx_, nullptr);
            pthread_cond_init(&cond_, nullptr);

            // 
            InitThreadPool();
        }

        static void *Rountine(void *args)
        {
            pthread_detach(pthread_self()); // 自动分离
            ThreadPool<T> *tp = (ThreadPool<T> *)args;

            while (true)
            {
                tp->Lock();
                while (tp->IsEmpty())
                {
                    // 任务队列为空,等待
                    tp->Wait();
                }

                T t;
                tp->PopTask(&t);
                std::cout << "执行任务:" << t << std::endl;
                tp->Unlock();
            }
        }

        void InitThreadPool()
        {
            pthread_t tid;
            for (int i = 0; i < num_; i++)
            {
                // 由于创建线程传入的参数是确定参数个数的
                // 所以该函数不能是类的成员函数,需要设置为静态函数
                pthread_create(&tid, nullptr, Rountine, this);
            }
        }

        void PushTask(const T &in)
        {
            Lock();
            task_queue_.push(in);
            Unlock();
            Wakeup();
        }

        void PopTask(T *out)
        {
            *out = task_queue_.front();
            task_queue_.pop();
        }
        ~ThreadPool()
        {
            pthread_cond_destroy(&cond_);
            pthread_mutex_destroy(&mtx_);
        }
    };
}

在这里插入图片描述

三、线程安全的单例模式

3.1 概念

单例模式是一种 “经典的, 常用的, 常考的” 设计模式.

单例模式的特点 :某些类, 只应该具有一个对象(实例), 就称之为单例.

饿汉实现方式和懒汉实现方式

[洗完的例子]
吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.

懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度.

饿汉方式实现单例模式

template <typename T>
class Singleton {
	static T data;
public:
	static T* GetInstance() {
		return &data;
	}
};
template <typename T>
T Singleton::data = T();

懒汉方式实现单例模式

template <typename T>
class Singleton {
	static T* inst;
public:
	static T* GetInstance() {
		if (inst == NULL) {
			inst = new T();
		}
		return inst;
	}
};

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

3.2 懒汉模式的线程池

#include "thread_pool.hpp"

using namespace ns_threadpool;

int main()
{
    while (true)
    {
        sleep(1);
        int t = rand()%10 + 1;
        std::cout << "send task: " << t << std::endl;
        ThreadPool<int>::GetInstance()->PushTask(t);
        //单例本身会在任何场景,任何环境下被调用
        //GetInstance():被多线程重入,进而导致线程安全的问题
        //std::cout << ThreadPool<int>::GetInstance() << std::endl;
    }
}
#pragma once

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

namespace ns_threadpool
{
    const int g_num = 5;
    template <class T>
    class ThreadPool
    {
    private:
        std::queue<T> task_queue_;
        int num_; // 线程池线程的数量

        pthread_mutex_t mtx_;
        pthread_cond_t cond_;

        // 指向唯一的对象,设为成员变量
        static ThreadPool<T>* ins;

    private:
        // 由于是单例模式,只有一个对象,不能在外面创建对象
        // 所以将构造函数设为私有
        // 构造函数必须得实现,但是必须的私有化
        ThreadPool(int num = g_num) : num_(num)
        {
            pthread_mutex_init(&mtx_, nullptr);
            pthread_cond_init(&cond_, nullptr);

            InitThreadPool();
        }
        ThreadPool(const ThreadPool<T> &tp) = delete;
        //赋值语句
        ThreadPool<T> &operator=(ThreadPool<T> &tp) = delete;
    private:
        void Lock()
        {
            pthread_mutex_lock(&mtx_);
        }
        void Unlock()
        {
            pthread_mutex_unlock(&mtx_);
        }

        void Wait()
        {
            pthread_cond_wait(&cond_, &mtx_);
        }
        void Wakeup()
        {
            pthread_cond_signal(&cond_);
        }
        bool IsEmpty()
        {
            return task_queue_.empty();
        }

    public:
        // 获取实例,也必须为静态函数才能获取
        static ThreadPool<T>* GetInstance()
        {
            // 为了防止多个线程同时调用,需要加锁保护
            static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
            // 检查当前对象是否被创建
            if (ins == nullptr)
            {
                // 双重判定,减少竞争锁的消耗
                pthread_mutex_lock(&mutex);
                if (ins == nullptr)
                {
                    ins = new ThreadPool<T>();
                    std::cout << "首次加载对象" << std::endl;
                }
                pthread_mutex_unlock(&mutex);
            }
            return ins;
        }



        static void *Rountine(void *args)
        {
            pthread_detach(pthread_self()); // 自动分离
            ThreadPool<T> *tp = (ThreadPool<T> *)args;

            while (true)
            {
                tp->Lock();
                while (tp->IsEmpty())
                {
                    // 任务队列为空,等待
                    tp->Wait();
                }

                T t;
                tp->PopTask(&t);
                std::cout << "执行任务:" << t << std::endl;
                tp->Unlock();
            }
        }

        void InitThreadPool()
        {
            pthread_t tid;
            for (int i = 0; i < num_; i++)
            {
                // 由于创建线程传入的参数是确定参数个数的
                // 所以该函数不能是类的成员函数,需要设置为静态函数
                pthread_create(&tid, nullptr, Rountine, this);
            }
        }

        void PushTask(const T &in)
        {
            Lock();
            task_queue_.push(in);
            Unlock();
            Wakeup();
        }

        void PopTask(T *out)
        {
            *out = task_queue_.front();
            task_queue_.pop();
        }
        ~ThreadPool()
        {
            pthread_cond_destroy(&cond_);
            pthread_mutex_destroy(&mtx_);
        }
    };

    // 类外初始化静态成员变量
    template <class T>
    ThreadPool<T> *ThreadPool<T>::ins = nullptr;
}

在这里插入图片描述

注意事项:

  1. 加锁解锁的位置
  2. 双重 if 判定, 避免不必要的锁竞争
  3. volatile关键字防止过度优化(ins)

四、读写者问题

4.1 概念

在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。

同样的对读写者模型有

  1. 一个缓冲区
  2. 两种角色(读者、写者)
  3. 三种关系
  • 读者和读者:没有关系
  • 写者和读者:互斥、同步
  • 写者和写者:互斥

读写者模型VS生产者消费者模型
本质:读者不会拿走数据,而消费者会拿走数据

什么情况适用读写者模型

  1. 对数据的大部分操作是读取,只有少量的操作是写入,也就是读者多于写者
  2. 判断读者一方是否会把资源拿走

4.2 相关接口

初始化

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 写者优先,但写者不能递归加锁
*/

五、自旋锁

5.1 自旋锁VS悲观锁

悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。前面经常使用的就是悲观锁
自旋锁:条件不满足时不会被挂起,而是反复询问。

使用场景

线程访问临界资源花费时长

  • 如果特别长 — 适合挂起等待(悲观锁)
  • 时间特别短 — 适合自旋锁(因为很快就能获得锁)

5.2 常用接口

相关操作和互斥锁类似
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s_persist

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值