项目八股[线程池]

发布时间 2023-09-09 10:17:44作者: timeMachine331

为什么要有线程池 因为频繁创建线程再销毁线程回收所有资源开销很大,所以项目中实现了一个线程池,线程池需要的做的事情就是维护任务队列与线程回调函数,工作线程即使在没有任务的情况下也不应该被回收,而是应该挂起等待唤醒。

所以总结一下要点:

1.线程池的实现和初始化:

1.任务如何提交打包。

2.任务队列的线程安全问题。

 

线程池这个类,在服务器程序启动之后,应该有且只有一个实例对象,因而需要以单例模式进行创建。

我在项目里使用的是懒汉式的局部静态变量实现的单例模式,局部静态变量保证了对象只有在第一次调用这个函数的时候会被初始化,之后的赋值操作都会被忽略并且它的生命周期会持续到程序的执行结束。

在C++11之前的懒汉式都会有线程安全问题创建的时候需要进行加锁,而 C++11 标准对于静态局部变量的初始化进行了互斥锁保护,保证了只有一个线程能对局部静态变量进行初始化操作其他线程再次进入时使用的都是已经初始化过的变量。而饿汉式的初始化是在程序开始的时候就进行资源的分配,不会有线程安全的问题。

线程池创建线程的时候需要让每个线程都立即执行一个回调函数并且阻塞在获取任务的为止。

 

任务如何提交打包,线程池这个类对象在初始化完成之后不应该直接被其他对象访问,应该只提供一个接口供主线程提交任务使用,将这个提交任务的函数设计成线程池的成员函数符合单一职能、和接口隔离的设计原则,不用考虑线程池内部的实现提升了封装性与安全性,也便于维护。

template<typename F>
    void addTask(F&& task)
    {
        mtxPool.lock();
        if ((int)tasks.size() < maxRequests)
        {
            //利用forward进行完美转发,保持右值引用属性
            tasks.emplace(forward<F>(task));
            condNotEmpty.signal();
        }
        mtxPool.unlock();
    }

线程池的任务提交函数时使用到了函数模板中的forward进行完美转发,保证了任务接收的参数的类型以及值都会被精准的转发给任务函数,也避免了不必要的参数拷贝,支持不同类型的任务保持了代码的简洁和可读性可维护性。

static void callback(ThreadPool* pool)
    {
        while(true)
        {
            pool->mtxPool.lock();
            while (pool->tasks.empty() && !pool->shutdown)
            {
                pool->condNotEmpty.wait(pool->mtxPool.get());
            }

            if (pool->shutdown)
            {
                pool->mtxPool.unlock();
                break;
            }
            
            //函数指针需要用move变成右值
            auto task = move(pool->tasks.front());
            pool->tasks.pop();
            pool->mtxPool.unlock();
            task(); //这里是用bind打包好的函数及其参数,可直接执行
        }
    }

回调函数里使用了move避免了从任务从任务队列里取出时进行不必要的拷贝,采用了移动构造转移了资源。类似的