Linux:线程池 | 自旋锁 | 读写锁

文章目录

  • 线程池
  • 懒汉模式
  • 自旋锁
  • 读写锁
  • 寄语

全文约 3036 字,预计阅读时长: 9分钟


线程池

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

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

  • 线程池示例:
    • 创建固定数量线程池,循环从任务队列中获取任务对象,
    • 获取到任务对象后,执行任务对象中的任务接口。
  • 小结:创建线程是有成本的,需要时再创建是比较慢的。所以线程池一次预先创建一大批线程,让这些线程处于“待机状态”;一旦由数据或者任务,直接可以交给线程去处理。

懒汉模式

  • 设计模式:大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案。
  • 单例模式:某些类, 只应该具有一个对象(实例)。
    • 饿汉模式:吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭。
    • 懒汉方式.:吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗。
  • 懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度。

  volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。

// 懒汉模式, 线程安全
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;
	}
};
  • 懒汉模式与内存池的结合:
    • 由于在类的内部使用成员函数,所以 要将线程的 handler 设置成 static。
    • 由于 static 的函数只能访问 static 的变量或函数,创建线程时函数的参数传 this ,就可以访问类的内部成员。

  pthread_detach()和pthread_join()就是控制子线程回收资源的两种不同的方式。同一进程间的线程具有共享和独立的资源,其中共享的资源有堆、全局变量、静态变量、文件等公用资源。而独享的资源有栈和寄存器,这两种方式就是决定子线程结束时如何回收独享的资源。

  如果是joinable状态,则该线程结束后(通过pthread_exit结束或者线程执行体任务执行完毕)不会释放线程所占用堆栈和线程描述符(总计8K多)等资源,除非在主线程调用了pthread_join函数之后才会释放。pthread_join函数一般应用在主线程需要等待子线程结束后才继续执行的场景。(pthread_join是一个阻塞函数,调用方会阻塞到pthread_join所指定的tid的线程结束后才被回收,但是在此之前,调用方是霸占系统资源的。 )

  如果是unjoinable状态,则该线程结束后会自动释放占用资源。实现方式是在创建时指定属性,或者在线程执行体的最开始处添加一行:pthread_detach(pthread_self());不会阻塞,调用它后,线程运行结束后会自动释放资源,后者非常方便。

#pragma once 

#include 
#include 
#include 

template <class T>
class ThreadPool{
    private:
        std::queue<T> q; //给线程池派发任务的地点, 临界资源
        pthread_mutex_t lock;
        pthread_cond_t cond;
    private:
        ThreadPool()
        {
            pthread_mutex_init(&lock, nullptr);
            pthread_cond_init(&cond, nullptr);
        }
        ThreadPool(const ThreadPool<T>&) = delete;
        ThreadPool<T>& operator = (const ThreadPool<T>&) = delete;
        static ThreadPool<T> *instance;
    public:
        static ThreadPool<T> *get_instance()
        {
            static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;  //静态初始化
            if(nullptr == instance)
            {
                pthread_mutex_lock(&mtx);
                if(nullptr == instance)
                {
                    instance = new ThreadPool<T>();
                }
                pthread_mutex_unlock(&mtx);
            }
            return instance;
        }
        void LockQueue()
        {
            pthread_mutex_lock(&lock);
        }
        void UnlockQueue()
        {
            pthread_mutex_unlock(&lock);
        }
        bool IsEmpty()
        {
            return q.size() == 0;
        }
        void ThreadWait()
        {
            pthread_cond_wait(&cond, &lock);
        }
        void ThreadWakeup()
        {
            pthread_cond_signal(&cond);
        }
        void PopTask(T *out)
        {
            *out = q.front();
            q.pop();
        }
        //Routinue是类中的一个成员方法!包含了一个隐士参数this!ThreadPool*
        //实际上,这里是包含了两个参数的!
        static void *Routinue(void *args/*,ThreadPool *this*/)
        {
            pthread_detach(pthread_self()); //线程分离
            ThreadPool *tp = (ThreadPool*)args;

            while(true){
                tp->LockQueue();
                //1. 检测是否有任务
                while(tp->IsEmpty())
                {
                    tp->ThreadWait(); 
                }
                //2. 取任务的过程
                T t;
                tp->PopTask(&t);
                tp->UnlockQueue();
                //3。处理任务
                t();
            }
        }
        void InitThreadPool(int num)
        {
            for(auto i = 0; i < num; i++){
                pthread_t tid;
                pthread_create(&tid, nullptr, Routinue, this);
            }
        }
        void PushTask(const T &in)
        {
            //放任务
            LockQueue();
            q.push(in);
            ThreadWakeup();
            UnlockQueue();
        }

        ~ThreadPool()
        {
            pthread_mutex_destroy(&lock);
            pthread_cond_destroy(&cond);
        }
};
template<class T>
ThreadPool<T>* ThreadPool<T>::instance = nullptr;
  • Task.hpp

  pthread_detach()即主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收。

  pthread_join()即是子线程合入主线程,主线程会一直阻塞,直到子线程执行结束,然后回收子线程资源,并继续执行。

#pragma once
#include 
#include 

class Task{
    private:
        int x;
        int y;
        char op; //+-*/%
    public:
        Task() //无参构造一定要有的
        {}
        Task(int _x, int _y, char _op):x(_x),y(_y),op(_op)
        {}
        void operator()()
        {
            run();
        }
        void run()
        {
            int z = -1;
            switch(op){
                case '+':
                    z = x + y;
                    break;
                case '-':
                    z = x - y;
                    break;
                case '*':
                    z = x * y;
                    break;
                case '/':
                    if(0 != y) z = x / y;
                    else std::cout << "Warning: div zero!" <<std::endl;
                    break;
                case '%':
                    if(0 != y) z = x % y;
                    else std::cout << "Warning: div zero!" <<std::endl;
                    break;
                default:
                    std::cout << "unknow operator!" << std::endl;
                    break;
            }
            std::cout <<"thread "<< "[" << pthread_self() << "] handler task done : "<< x << op << y << "=" << z << std::endl;
        }
        ~Task(){}
};
  • main.cc
#include "thread_pool.hpp"
#include "task.hpp"
#include 
#include 
#define NUM 5

int main()
{
    srand((unsigned)time(nullptr));
    ThreadPool<Task> *tp = ThreadPool<Task>::get_instance();
    tp->InitThreadPool(5);
    sleep(3);
    
    const std::string ops = "+-*/%";
    while(true){
        int x = rand() % 50 + 1;
        int y = rand() % 50 + 1;
        char op = ops[rand()%5];

        Task t(x, y, op);
        tp->PushTask(t);
        sleep(1);
    }
    return 0;
}

自旋锁

  • 是否采用自旋锁,取决于上一个线程在临界区中执行的时长。挂起是有成本的。
    Linux:线程池 | 自旋锁 | 读写锁_第1张图片

读写锁

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

  • 读者与读者之间没有关系;写者和写者之间是互斥关系;读者和写者是同步互斥的关系。
    Linux:线程池 | 自旋锁 | 读写锁_第2张图片
  • 读者优先:读者和写者一起到来时,会让读者优先进入。
  • 写者优先:当写者到来时,后续读者就暂时不能进入临界资源进行读取了,所有正在读取的线程执行完毕,写着再进入。
  • 综上:谁优先,取决于是要读到新数据,还是旧数据。

寄语

  • 先这样吧…

你可能感兴趣的:(Linux,单例模式,开发语言,linux,后端,c++)