Bootstrap

单例及线程池的实现及感悟分享

        

碎碎念:

        有快3个月没有写博客了,这段时间很多事,抽不出空来写博客。后面的博客可能风格也会转变,从理论到实战为主,尽量减少源码的解读。(看完源码后面自己也忘了QAQ),本篇博客主要介绍线程池的实现及其前置单例模式的一些实现方法,其中使用了恋恋辰风大佬的一些例子。

单例模式

        在众多的设计模式中,尽管因为语言特性而使某些设计模式缺乏实用性。但是单例模式却是使用最为广泛的设计模式之一。在需要大规模数据通信的场景,又或者需要重复构造释放以及各种工厂模式,句柄。单例模式都在其中展现出了优势。例如,如果你想进行IPC通信,如果仅仅是处理轻量级的数据交换或同步任务,那么你可以选择使用future,task,promise等,但是如果是大规模数据,实现一个单例缓冲区Buffer,构造一个生产者消费者模型则是方法之一。值得一提的是,在观看boost库的实现中,我发现了一种写法可以些许提高性能,也就是消费者自身维护一个buffer,在消费完之后,直接和单例类中的buffer进行swap,而无需从单例buffer中取数据,这种方法可以减少锁的竞争和访问延迟。。单例模式一般常见的来说有懒汉式和饿汉式两种,懒汉式就是在getInstance函数执行的时候如果未创建单例,则创建单例。饿汉式则是将单例模式的创建放在了构造函数当中。如果是单线程无所谓,但是在多线程的情况下,我并不是很推荐使用懒汉式(即使他使用双锁)。这是因为new本身不是一个原子操作,他可能出现重排序问题从而先返回了但是还没实际构建,这样的风险非常大。而懒汉式由于是在构造函数中实现则不存在这个问题。这两种我都不是很常用,我常用的两种一种是返回局部变量,这种非常简单,而且可靠性较高,但是注意不要在C++11标准之前使用,会有风险,而11之后修复了这个问题。第二种是使用once_flag和call_once配合来实现,call_once会保证once_flag只被初始化一次。下面来看一下具体的实现,为了观看方便我就不分开写了:

#include<mutex>
#include<memory>

class SingleClass {

public:
	
	static SingleClass& getInstance() {
		static SingleClass instance;
		return instance;
	}
	static std::shared_ptr<SingleClass> getInstance2() {
		std::call_once(flag_, [&]() {
			instance_ = std::make_shared<SingleClass>();
			});
			return instance_;
	}
private:
	static std::once_flag flag_;
	SingleClass() = default;
	SingleClass(const SingleClass&) = delete;
	SingleClass& operator =(const SingleClass&) = delete;
	static std::shared_ptr<SingleClass>  instance_;
};

std::once_flag SingleClass::flag_;
std::shared_ptr<SingleClass> SingleClass::instance_ = nullptr;

        这里有几个注意的点,getInstance都要返回引用,一定一定一定要返回引用,不然他就是走的拷贝,我们已经删除了就过不了而且就算是过了也不能走拷贝。

        如果需要单例多可以写个模板:

#pragma once
#include <memory>
#include <mutex>
#include <iostream>
using namespace std;

template<typename T>
class Singleton{

public:
    static shared_ptr<T> getInstance(){
        static std::once_flag s_flag;
        std::call_once(s_flag,[&]{
            instance_ = make_shared<T>();
        });
        return instance_;
    }
    
protected:
    Singleton()=default;
    Singleton(const Singleton& ) = delete;
    Singleton& operator= (const Singleton& ) = delete;
    static shared_ptr<T> instance_; 
};
template<typename T>
shared_ptr<T> Singleton<T>::instance_ = nullptr;

这个只要继承就好了,他比较好的就是继承之后不用删除拷贝构造和重写赋值了,因为构造由内而外,先构造父类,父类那里直接禁了子类也用不了这俩了。但是需要注意的是,如果子类自身的构造函数放在private里记得把这个模板设为友元,不然拿不到构造函数。

线程池

        线程池的主要功能是管理线程的生命周期,以减少频繁创建和销毁线程的开销。在线程池创建时,会启动一定数量的线程,并让这些线程通过任务队列来获取待执行的任务。工作线程会不断地循环获取任务并执行,从而提高了任务处理的效率。线程池在销毁时,确保所有子线程都完成任务后才退出,这一点非常重要。如果主线程在子线程还未完成任务时就退出,可能会导致未完成的任务丢失或程序状态不一致。因此,在设计线程池时,必须确保在主线程退出之前,所有子线程的任务都已经处理完毕。在多线程程序设计中,通常需要确保主线程在所有子线程完成任务后才退出。线程池通过管理工作线程的生命周期和同步机制(如使用`join`或条件变量)确保所有任务完成后,主线程才能安全退出。我们来看一下主要的实现,这里使用了恋恋辰风博主写的一个线程池,只完成了主要功能。也便于理解:

线程池主要代码

#ifndef __THREAD_POOL_H__
#define __THREAD_POOL_H__

#include <atomic>
#include <condition_variable>
#include <future>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>

class NoneCopy {

public:
    ~NoneCopy(){}
protected:
    NoneCopy(){}
private:
    NoneCopy(const NoneCopy&) = delete;
    NoneCopy& operator=(const NoneCopy&) = delete;
};

class ThreadPool : public NoneCopy {
public:
    //继承基类NoneCopy就不需要写如下删除了
    //ThreadPool(const ThreadPool&) = delete;
    //ThreadPool& operator=(const ThreadPool&) = delete;

    static ThreadPool& instance() {
        static ThreadPool ins;
        return ins;
    }

    using Task = std::packaged_task<void()>;


    ~ThreadPool() {
        stop();
    }

    template <class F, class... Args>
    auto commit(F&& f, Args&&... args) -> 
        std::future<decltype(std::forward<F>(f)(std::forward<Args>(args)...))> {
        using RetType = decltype(std::forward<F>(f)(std::forward<Args>(args)...));
        if (stop_.load())
            return std::future<RetType>{};

        auto task = std::make_shared<std::packaged_task<RetType()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...));

        std::future<RetType> ret = task->get_future();
        {
            std::lock_guard<std::mutex> cv_mt(cv_mt_);
            tasks_.emplace([task] { (*task)(); });
        }
        cv_lock_.notify_one();
        return ret;
    }

    int idleThreadCount() {
        return thread_num_;
    }

private:
    ThreadPool(unsigned int num = std::thread::hardware_concurrency())
        : stop_(false) {
            {
                if (num <= 1)
                    thread_num_ = 2;
                else
                    thread_num_ = num;
            }
            start();
    }
    void start() {
        for (int i = 0; i < thread_num_; ++i) {
            pool_.emplace_back([this]() {
                while (!this->stop_.load()) {
                    Task task;
                    {
                        std::unique_lock<std::mutex> cv_mt(cv_mt_);
                        this->cv_lock_.wait(cv_mt, [this] {
                            return this->stop_.load() || !this->tasks_.empty();
                            });
                        if (this->tasks_.empty())
                            return;

                        task = std::move(this->tasks_.front());
                        this->tasks_.pop();
                    }
                    this->thread_num_--;
                    task();
                    this->thread_num_++;
                }
                });
        }
    }
    void stop() {
        stop_.store(true);
        cv_lock_.notify_all();
        for (auto& td : pool_) {
            if (td.joinable()) {
                std::cout << "join thread " << td.get_id() << std::endl;
                td.join();
            }
        }
    }

private:
    std::mutex               cv_mt_;
    std::condition_variable  cv_lock_;
    std::atomic_bool         stop_;
    std::atomic_int          thread_num_;
    std::queue<Task>         tasks_;
    std::vector<std::thread> pool_;
};

#endif  // !__THREAD_POOL_H__

       继承

class NoneCopy {

public:
    ~NoneCopy(){}
protected:
    NoneCopy(){}
private:
    NoneCopy(const NoneCopy&) = delete;
    NoneCopy& operator=(const NoneCopy&) = delete;
};

        这里继承也就是我们前面说的模板继承方式,不过他并没有在模板类中创建getinsance而是禁止拷贝构造和拷贝赋值。往下看

    static ThreadPool& instance() {
        static ThreadPool ins;
        return ins;
    }

    using Task = std::packaged_task<void()>;


    ~ThreadPool() {
        stop();
    }

        第一个没什么好说的 ,析构也没什么好说的,主要看第二个Task,他可不是乱写的哈,这里是因为投递的任务返回类型不同,参数也不同统一拿void()包装一下,内部函数就是通过Bind绑定过参数的任务了,直接调用就好了。对了这里要给不太了解的朋友说一下,这个packaged_task可以和future绑定,只要返回future,使用线程池调用者就可以通过future拿到函数运行后的返回值。接下来看看构造函数:

构造函数

ThreadPool(unsigned int num = std::thread::hardware_concurrency())
        : stop_(false) {
            {
                if (num <= 1)
                    thread_num_ = 2;
                else
                    thread_num_ = num;
            }
            start();
    }

        这里也没什么好说的,建议是开的线程数等于你的CPU核数。过多的线程会造成线程竞争,浪费时间片。但是1核我们也不能让他太寒碜变成单线程了吧,至少开两个吧。。。

 start函数

    void start() {
        for (int i = 0; i < thread_num_; ++i) {
            pool_.emplace_back([this]() {
                while (!this->stop_.load()) {
                    Task task;
                    {
                        std::unique_lock<std::mutex> cv_mt(cv_mt_);
                        this->cv_lock_.wait(cv_mt, [this] {
                            return this->stop_.load() || !this->tasks_.empty();
                            });
                        if (this->tasks_.empty())
                            return;

                        task = std::move(this->tasks_.front());
                        this->tasks_.pop();
                    }
                    this->thread_num_--;
                    task();
                    this->thread_num_++;
                }
                });
        }
    }

        这也就是线程就射设置线程,让他循环的执行任务,{}作用域的作用主要是提前结束对共享资源的掌控,要是想用延迟锁也可以,直接让他加完锁再解锁,只是这种方便一点。而且对于lock_guard这种不支持加锁解锁的,也可以控制加锁范围。非常方便十分推荐。这里需要注意的是如果他等待挂起的时候如果是准备退出了,那么他就处理完自己的函数就退出了。忘了说了这里一定要用emlace_back。因为push_back其实是拷贝,那就会存在两个线程一个还回收不掉。

stop函数

    void stop() {
        stop_.store(true);
        cv_lock_.notify_all();
        for (auto& td : pool_) {
            if (td.joinable()) {
                std::cout << "join thread " << td.get_id() << std::endl;
                td.join();
            }
        }
    }

       这里也没什么好说的,就是设置退出标志服,然后唤醒所有挂起线程,再回收。

Commit函数

        这是线程池中最关键的函数,为什么不同返回值什么不同的参数函数可以投递到一个队列中.我们来看一下。

template <class F, class... Args>
    auto commit(F&& f, Args&&... args) -> 
        std::future<decltype(std::forward<F>(f)(std::forward<Args>(args)...))> {
        using RetType = decltype(std::forward<F>(f)(std::forward<Args>(args)...));
        if (stop_.load())
            return std::future<RetType>{};

        auto task = std::make_shared<std::packaged_task<RetType()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...));

        std::future<RetType> ret = task->get_future();
        {
            std::lock_guard<std::mutex> cv_mt(cv_mt_);
            tasks_.emplace([task] { (*task)(); });
        }
        cv_lock_.notify_one();
        return ret;
    }

        首先他使用了auto来确定返回值的类型,怎么做到的呢?

auto commit(F&& f, Args&&... args) 
-> std::future<decltype(std::forward<F>(f)(std::forward<Args>(args)...))>

这个首先他返回的是一个future嘛,这样调用者才可以通过future拿到结果。里面就是用decltype确定类型,什么类型呢?f函数调用的类型。后面args是参数,...是展开。这样就可以输入多个参数然后确定类型了。注意这里一定要用完美转发来确保他的参数左值右值性质不变。往后就是如果线程准备退出了就直接return。

        auto task = std::make_shared<std::packaged_task<RetType()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...));

        std::future<RetType> ret = task->get_future();
        {
            std::lock_guard<std::mutex> cv_mt(cv_mt_);
            tasks_.emplace([task] { (*task)(); });
        }

        然后task用bind进行绑定参数,这样就可以不输入参数直接调用了,但是他还有返回值怎么办呢?那就用lambda表达式直接包装一层就行了,注意这里一定要捕获task要不就&,最好不要捕获this。不然会导致潜在的访问悬空指针的问题,除非你确保this在任务执行期间依然有效。就算他是单例模式,在主程序挂掉之前不会失效,我仍然还是建议不要捕获this。然后往下直接唤醒返回就行了。

结语

        放假之后抽空写了一下自己的一些理解和感悟,希望后面能找一个好一点的实习。后面主要会写车载网络的一些知识,也穿插一下和普通TCP/IP的区别,DDS和SOME/IP的一些实战。源码还是等工作后有空再写吧。

;