碎碎念:
有快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的一些实战。源码还是等工作后有空再写吧。