当前位置: 首页 > news >正文

【Linux】简单线程池的设计与实现 -- 单例模式

  • 前言
  • 对锁的封装
    • 整体代码
    • LockGuard - RALL
    • RALL
    • Mutex封装
  • 对线程创建的封装
    • 整体代码
    • 成员函数解释声明
  • 业务处理封装-加减乘除(可有可无)
    • 整体代码
    • 成员函数解释声明
  • 线程池的设计与实现
    • 整体代码
    • 成员函数解释声明
  • 展示

前言

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

线程池的应用场景:

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

线程池示例:

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

对锁的封装

整体代码

LockGuard.hpp

#pragma once
#include <iostream>
#include <pthread.h>class Mutex
{
public:Mutex(pthread_mutex_t *lock_p = nullptr) : lock_p_(lock_p){}void lock(){if (lock_p_)pthread_mutex_lock(lock_p_);}void unlock(){if (lock_p_)pthread_mutex_unlock(lock_p_);}~Mutex(){}private:pthread_mutex_t *lock_p_;
};class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex) : mutex_(mutex){mutex_.lock(); // 在构造函数中进行加锁}~LockGuard(){mutex_.unlock(); // 在析构函数中进行解锁}private:Mutex mutex_;
};

LockGuard - RALL

对锁的封装

class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex) : mutex_(mutex){mutex_.lock(); // 在构造函数中进行加锁}~LockGuard(){mutex_.unlock(); // 在析构函数中进行解锁}
private:Mutex mutex_;
};

定义了一个名为 LockGuard 的类,它实现了 RAII(Resource Acquisition Is Initialization)技术,用于自动加锁和解锁互斥量

具体来说,LockGuard 类的构造函数接受一个指向 pthread_mutex_t 类型的指针作为参数,并在函数内部将该指针封装成一个 Mutex 对象并进行加锁操作。在 LockGuard 对象创建时,该互斥量将被自动加锁,确保线程安全。

LockGuard 类的析构函数在对象销毁时自动调用,并在函数内部对互斥量进行解锁操作。在 LockGuard 对象销毁时,该互斥量将被自动解锁,释放锁资源。

通过使用 LockGuard 类,可以避免手动加锁和解锁互斥量,从而减少代码出错的可能性,并确保线程安全。同时,使用 RAII 机制可以提高代码的可读性和可维护性,使得程序更加健壮和可靠。

RALL

RAII(Resource Acquisition Is Initialization)技术是一种 C++ 语言的编程技术,它的基本思想是:在对象的构造函数中获取资源,在对象的析构函数中释放资源,从而实现资源的自动管理。

RAII 技术通常用于管理资源对象,例如内存、文件句柄、互斥锁、临界区等等,这些资源需要在使用完成后手动释放,否则会导致资源泄露或者资源使用冲突等问题。

使用 RAII 技术,可以避免手动管理资源带来的繁琐和容易出错的问题。RAII 技术的关键是利用对象的构造函数和析构函数来自动管理资源,当对象超出作用域时,其析构函数自动被调用,从而释放资源。

在 C++ 中,STL 中的智能指针和标准库中的各种容器,例如 vector、map、set等都是使用 RAII 技术实现的,它们可以自动管理内存和容器元素的生命周期,从而避免了手动管理资源的繁琐和容易出错的问题。

Mutex封装

定义了一个名为 Mutex 的类,它封装pthread_mutex_t 互斥量,并提供了 lock()unlock() 成员函数,用于加锁和解锁互斥量

#pragma once
#include <iostream>
#include <pthread.h>class Mutex
{
public:Mutex(pthread_mutex_t *lock_p = nullptr) : lock_p_(lock_p){}void lock(){if (lock_p_)pthread_mutex_lock(lock_p_);}void unlock(){if (lock_p_)pthread_mutex_unlock(lock_p_);}~Mutex(){}private:pthread_mutex_t *lock_p_;
};

Mutex 类的构造函数接受一个指向 pthread_mutex_t 类型的指针作为参数,并在函数内部将该指针保存到成员变量 lock_p_ 中。如果构造函数没有接收到任何参数,则将 lock_p_ 初始化为 nullptr

Mutex 类的 lock() 成员函数用于加锁互斥量。它首先检查 lock_p_ 是否为 nullptr,如果不是,则调用 pthread_mutex_lock() 函数对互斥量进行加锁操作。

Mutex 类的 unlock() 成员函数用于解锁互斥量。它首先检查 lock_p_ 是否为 nullptr,如果不是,则调用 pthread_mutex_unlock() 函数对互斥量进行解锁操作。

通过使用 Mutex 类,可以封装 pthread_mutex_t 互斥量,并提供了方便的 lock() 和 unlock() 成员函数,用于加锁和解锁互斥量。这使得代码更加简洁和易于管理,并确保线程安全。同时,Mutex 类的实现也符合 RAII 技术,可以自动释放锁资源,从而减少代码出错的可能性。


对线程创建的封装

整体代码

Thread.hpp

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <functional>
#include <pthread.h>namespace ThreadNs
{typedef std::function<void *(void *)> func_t;const int num = 1024;class Thread{private:static void *start_routine(void *args) // 类内成员,有缺省参数!{Thread *_this = static_cast<Thread *>(args);return _this->callback();}public:Thread(){char namebuffer[num];snprintf(namebuffer, sizeof namebuffer, "thread-%d", threadnum++);name_ = namebuffer;}void start(func_t func, void *args = nullptr){func_ = func;args_ = args;int n = pthread_create(&tid_, nullptr, start_routine, this); assert(n == 0);(void)n;}void join(){int n = pthread_join(tid_, nullptr);assert(n == 0);(void)n;}std::string threadname(){return name_;}~Thread(){// do nothing}void *callback() { return func_(args_); }private:std::string name_; // 线程名字func_t func_;      // 任务处理函数void *args_;       // 任务处理函数的参数pthread_t tid_;    // 线程的 IDstatic int threadnum; // 计算线程个数,用于格式化线程名字为其增加编号};int Thread::threadnum = 1;
}

成员函数解释声明

typedef std::function<void *(void *)> func_t;

定义了一个名为 func_t 的类型别名,它是一个函数对象类型,用于封装一个可调用对象,接受一个指针参数并返回一个指针值

static void *start_routine(void *args) // 类内成员,有缺省参数!
{Thread *_this = static_cast<Thread *>(args);return _this->callback();
}

在类内创建线程,想让线程执行对应的方法,需要将方法设置成为static

作用是创建并初始化线程
Thread()
{char namebuffer[num];snprintf(namebuffer, sizeof namebuffer, "thread-%d", threadnum++);name_ = namebuffer;
}

构造函数Thread(),它的作用是创建并初始化线程
定义了一个字符数组 namebuffer,用来存储线程的名称。然后,使用 snprintf 函数将线程的名称格式化成字符串,并将该字符串存储在 namebuffer 中。其中,threadnum 是一个静态变量,用来记录已经创建的线程数量,每次创建线程时都会将 threadnum 加 1,从而保证每个线程都有一个唯一的名称

启动一个新的线程并开始执行指定的任务处理函数
void start(func_t func, void *args = nullptr)
{func_ = func;args_ = args;// 调用 pthread_create() 函数创建一个新的线程,并将线程 ID 保存到成员变量 tid_ 中int n = pthread_create(&tid_, nullptr, start_routine, this); // TODOassert(n == 0);(void)n;
}

定义了 Thread 类的成员函数 start(),它的作用是启动一个新的线程并开始执行指定的任务处理函数。一个任务处理函数指针 func 和一个可选的参数 args,表示任务处理函数的参数。

等待线程执行结束并回收线程资源。
void join()
{int n = pthread_join(tid_, nullptr);assert(n == 0);(void)n;
}

业务处理封装-加减乘除(可有可无)

整体代码

Task.hpp

#pragma once#include <iostream>
#include <string>
#include <cstdio>
#include <functional>class Task
{using func_t = std::function<int(int, int, char)>;
public:Task(){}Task(int x, int y, char op, func_t func): _x(x), _y(y), _op(op), _callback(func){}std::string operator()(){int result = _callback(_x, _y, _op);char buffer[1024];snprintf(buffer, sizeof buffer, "%d %c %d = %d", _x, _op, _y, result);return buffer;}std::string toTaskString(){char buffer[1024];snprintf(buffer, sizeof buffer, "%d %c %d = ?", _x, _op, _y);return buffer;}private:int _x;int _y;char _op;func_t _callback;
};const std::string oper = "+-*/%";int mymath(int x, int y, char op)
{int result = 0;switch (op){case '+':result = x + y;break;case '-':result = x - y;break;case '*':result = x * y;break;case '/':{if (y == 0){std::cerr << "div zero error!" << std::endl;result = -1;}elseresult = x / y;}break;case '%':{if (y == 0){std::cerr << "mod zero error!" << std::endl;result = -1;}elseresult = x % y;}break;default:// do nothingbreak;}return result;
}

成员函数解释声明

using func_t = std::function<int(int, int, char)>;

一个函数对象类型,用于封装一个可调用对象,接受两个 int 类型参数和一个 char 类型参数,并返回一个 int 类型值。

具体来说,func_t 是一个 std::function 模板类的实例化类型,它接受一个函数类型作为模板参数,其中该函数类型接受两个 int 类型参数和一个 char 类型参数,分别表示运算符两侧的操作数和运算符本身,并返回一个 int 类型值,表示运算结果

因此,func_t 类型可以用于封装一个可调用对象,该对象接受两个 int 类型参数和一个 char 类型参数,并返回一个 int 类型值,类似于 C 语言中的函数指针类型。

在本例中,func_t 类型被用作 Task 类的成员变量 _callback 的类型,它用于封装一个计算加减乘除的回调函数,该函数接受两个 int 类型参数和一个 char 类型参数,并返回一个 int 类型值。在 Task 类的成员函数中,可以通过调用成员变量 _callback 来调用回调函数,并传递参数。

它的作用是执行一个回调函数,并将结果以字符串的形式返回。
std::string operator()()
{int result = _callback(_x, _y, _op);char buffer[1024];snprintf(buffer, sizeof buffer, "%d %c %d = %d", _x, _op, _y, result);return buffer;
}

定义了一个函数调用运算符的重载函数 operator()
调用成员变量 _callback 所指向的回调函数,传递成员变量 _x、_y、_op 作为参数,并将返回值存储在 result 变量中。使用 snprintf 函数将计算结果格式化为一个字符串,并将其存储在 buffer 数组中。最后,它将 buffer 数组转换为一个 std::string 对象,并返回该对象。

作用是将任务的信息格式化成字符串并返回。
std::string toTaskString()
{char buffer[1024];snprintf(buffer, sizeof buffer, "%d %c %d = ?", _x, _op, _y);return buffer;
}

使用了 C 标准库中的 sprintf 函数将 _x、_op 、_y 这三个成员变量格式化成字符串,并将结果存储在字符数组 buffer 中。其中,_x 、_y 分别表示任务要操作的两个数,_op 表示操作符(例如加号、减号等)。然后,通过 std::string 类型的构造函数将字符数组 buffer 转换成字符串,并将该字符串作为函数的返回值。


线程池的设计与实现

整体代码

ThreadPool.hpp

#pragma once
#include "Thread.hpp"
#include "LockGuard.hpp"
#include <vector>
#include <queue>
#include <mutex>
#include <pthread.h>
#include <unistd.h>using namespace ThreadNs;
const int gnum = 3;template <class T>
class ThreadPool;template <class T>
class ThreadData
{
public:ThreadPool<T> *threadpool;std::string name;public:ThreadData(ThreadPool<T> *tp, const std::string &n) : threadpool(tp), name(n){}
};
template <class T>
class ThreadPool
{
private:static void *handlerTask(void *args){ThreadData<T> *td = (ThreadData<T> *)args;while (true){T t;{LockGuard lockguard(td->threadpool->mutex()); while (td->threadpool->isQueueEmpty()){td->threadpool->threadWait();}t = td->threadpool->pop(); }std::cout << td->name << " 获取了一个任务: " << t.toTaskString() << " 并处理完成,结果是:"<< t() << std::endl;}delete td; // 删除new出的对象return nullptr;}ThreadPool(const int &num = gnum) : _num(num){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);for (int i = 0; i < _num; i++){_threads.push_back(new Thread());}}void operator=(const ThreadPool &) = delete;ThreadPool(const ThreadPool &) = delete;public:void lockQueue() { pthread_mutex_lock(&_mutex); }void unlockQueue() { pthread_mutex_unlock(&_mutex); }bool isQueueEmpty() { return _task_queue.empty(); }void threadWait() { pthread_cond_wait(&_cond, &_mutex); }T pop(){T t = _task_queue.front();_task_queue.pop();return t;}pthread_mutex_t *mutex(){return &_mutex;}public:void run(){for (const auto &t : _threads){ThreadData<T> *td = new ThreadData<T>(this, t->threadname());t->start(handlerTask, td);std::cout << t->threadname() << " start ..." << std::endl;}}void push(const T &in){LockGuard lockguard(&_mutex); _task_queue.push(in);         pthread_cond_signal(&_cond);}// 析构函数~ThreadPool(){pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond);   for (const auto &t : _threads)  delete t;}static ThreadPool<T> *getInstance(){if (nullptr == tp){_singlock.lock();if (nullptr == tp){tp = new ThreadPool<T>();}_singlock.unlock();}return tp;}
private:int _num;std::vector<Thread *> _threads; // 存放创建的线程实例的指针std::queue<T> _task_queue;      // 存储待执行的任务。它是一个先进先出(FIFO)的队列pthread_mutex_t _mutex;         // pthread 库中的互斥锁类型,用于保护任务队列的访问pthread_cond_t _cond;           // pthread 库中的条件变量类型,用于线程之间的同步和通信。// 在任务队列为空时,线程需要等待条件变量的信号,以便在有新的任务加入时立即处理。// 而当任务加入队列时,需要发送条件变量的信号,以通知等待的线程有新的任务可以处理了。// tp 是一个指向 ThreadPool 类的指针,它是静态变量,只有一个实例,用于在整个程序中共享线程池的实例。// 由于线程池是一个全局的资源,需要确保所有的线程都共享同一个实例,避免资源浪费和线程安全问题。static ThreadPool<T> *tp; // 指向 ThreadPool 类的指针 tp// 由于线程池是一个单例模式,需要确保只有一个实例被创建,避免资源浪费和线程安全问题。因此,当多个线程同时访问时,// 需要使用互斥锁对其进行保护,避免多个线程同时创建线程池实例static std::mutex _singlock; // std::mutex 类型的互斥锁 _singlock。语言层面
};// tp 是一个指向 ThreadPool 类对象的指针,初始值为 nullptr。
// 它被用于实现 ThreadPool 类的单例模式,确保每个程序只有一个 ThreadPool 类对象。
template <class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;// _singlock 是一个互斥量,用于保护对静态成员变量 tp 的访问。
// 它被用于实现线程安全的单例模式,避免多个线程同时创建 ThreadPool 类对象,导致资源浪费和线程安全问题。
template <class T>
std::mutex ThreadPool<T>::_singlock;

成员函数解释声明

template <class T>
class ThreadData
{
public:ThreadPool<T> *threadpool;std::string name;
public:ThreadData(ThreadPool<T> *tp, const std::string &n) : threadpool(tp), name(n){}
};

定义了一个名为ThreadData 的模板类,用于封装线程池和线程之间的关系,保存了线程池的指针和线程的名称。

一个指向线程池的指针 threadpool,一个表示线程名称的字符串 name。它们分别用于保存线程池的指针和线程的名称。

接受两个参数,分别是线程池的指针 tp 和线程的名称 n构造函数将这两个参数保存到 ThreadData 对象的成员变量中,以便在后续的线程处理函数中使用。

通过创建一个 ThreadData<T> 类型的对象,并将线程池的指针和线程的名称传递给其构造函数,可以封装线程池和线程之间的关系,并确保线程池的正确运行。在启动线程时,会将该对象的指针传递给线程,并在处理任务时使用其中的线程池指针。这样可以简化线程池的实现,提高线程池的可维护性和可扩展性。


作为线程的入口函数,循环从任务队列中获取任务并执行。

static void *handlerTask(void *args){ThreadData<T> *td = (ThreadData<T> *)args;while (true){//定义一个类型为 T 的变量 t,用来存储从任务队列中获取到的任务。T t;{LockGuard lockguard(td->threadpool->mutex()); while (td->threadpool->isQueueEmpty()){td->threadpool->threadWait();}t = td->threadpool->pop(); }// 格式化输出信息std::cout << td->name << " 获取了一个任务: " << t.toTaskString() << " 并处理完成,结果是:"<< t() << std::endl;}delete td; // 删除new出的对象return nullptr;}
  • LockGuard lockguard(td->threadpool->mutex());
    这段代码使用了 RAII(Resource Acquisition Is Initialization)技术,通过定义一个局部变量 LockGuard 对象 lockguard,实现了自动获取和释放互斥锁的操作。LockGuard 是一个 RAII类模板,它的构造函数会在对象创建时获取互斥锁,析构函数会在对象销毁时释放互斥锁。这样,在函数执行过程中,只需要将需要保护的代码块放在 LockGuard 对象的作用域内,就可以保证在退出作用域时自动释放互斥锁,避免了手动释放锁的繁琐操作和可能的遗漏。
    在这段代码中,LockGuard 对象 lockguard 的构造函数被调用时,传入了一个指向互斥锁的指针 td->threadpool->mutex(),表示获取该互斥锁。当 lockguard 对象的作用域结束时,析构函数自动释放该互斥锁,这样就保证了在访问任务队列时的线程安全。

  • while (td->threadpool->isQueueEmpty())
    从任务队列中获取任务并执行,它的核心是一个循环,不断尝试从任务队列中获取任务,直到获取到任务为止。

  • td->threadpool->threadWait();
    将当前线程挂起,等待有新的任务加入队列或者线程被停止。如果队列不为空,则执行下一步操作。

  • t = td->threadpool->pop();
    从任务队列中取出一个任务,并将其赋值给变量 t。这个 pop 方法的本质是将任务从公共队列中拿到,当前线程自己独立的栈中,以避免多个线程同时访问同一个任务对象的线程安全问题。

ThreadPool(const int &num = gnum) : _num(num)
{pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);for (int i = 0; i < _num; i++){_threads.push_back(new Thread());}
}

ThreadPool构造函数,它的作用是创建并初始化线程池。接受一个 int 类型的参数num,用来指定线程池中线程的数量。如果没有传入 num,就使用全局变量 gnum 的默认值。然后,构造函数初始化了线程池中的互斥量和条件变量,以及线程池的大小。

使用 pthread_mutex_init 函数初始化互斥量 _mutex,使用 pthread_cond_init 函数初始化条件变量 _cond,这两个函数都是 POSIX 线程库中的函数,用来创建和初始化互斥量和条件变量。

使用一个 for 循环创建线程池中的线程。每次循环创建一个 Thread 类的实例,并将其指针添加到线程池的 _threads 向量中。这个 _threads 向量是用来存储线程池中所有线程的指针的,它的大小就是线程池的大小,即 num

void operator=(const ThreadPool &) = delete;
ThreadPool(const ThreadPool &) = delete;

定义了 ThreadPool 类的拷贝赋值运算符和拷贝构造函数,并将它们声明为 delete表示禁止进行拷贝构造和拷贝赋值操作。

因为线程池是一个资源管理类,它包含了多个线程、任务队列、互斥锁、条件变量等资源,这些资源的管理和释放都需要仔细考虑。如果允许进行拷贝构造和拷贝赋值操作,就会导致多个对象共享同一份资源,可能会引起资源泄露、线程安全问题等等。

为了避免这种情况,通常会将拷贝构造函数和拷贝赋值运算符声明为 delete,表示禁止进行拷贝操作。这样,就可以确保每个线程池都拥有自己独立的资源,避免了资源共享带来的问题。需要注意的是,这段代码中使用了 C++11 中的新特性,即使用 “= delete” 显式声明某个函数为删除函数。这种方式可以在编译期间检查是否存在拷贝构造和拷贝赋值操作,避免了在运行时出现错误。

void lockQueue() { pthread_mutex_lock(&_mutex); }

使用 pthread_mutex_lock 函数对任务队列的互斥锁进行加锁操作,以保证在多个线程同时访问时不会出现冲突。

void unlockQueue() { pthread_mutex_unlock(&_mutex); }

使用 pthread_mutex_unlock 函数对任务队列的互斥锁进行解锁操作,以释放资源并让其它线程获得访问权限。

bool isQueueEmpty() { return _task_queue.empty(); }

用于判断任务队列是否为空,它返回一个 bool 类型的值,如果任务队列为空则返回 true,否则返回 false。

void threadWait() { pthread_cond_wait(&_cond, &_mutex); }

使用 pthread_cond_wait函数将线程放入等待状态,并等待条件变量的信号。该函数将在 _cond 条件变量上等待,并同时释放 _mutex 互斥锁,以便其它线程可以获得访问权限。当条件变量的信号发生时,该函数将重新获得 _mutex 互斥锁,并继续执行后续的任务处理操作。

将队列中的任务弹出,并返回弹出值
T pop()
{T t = _task_queue.front();_task_queue.pop();return t;
}
返回一个指向线程池的互斥锁的指针。
pthread_mutex_t *mutex()
{return &_mutex;
}

返回互斥锁的指针可以使得其它函数可以方便地对互斥锁进行加锁和解锁操作,以保证线程安全。同时,通过返回指针的方式,也避免了多余的复制操作和内存开销。

启动线程池中的所有线程,并开始处理任务。
void run()
{for (const auto &t : _threads){ThreadData<T> *td = new ThreadData<T>(this, t->threadname());t->start(handlerTask, td);std::cout << t->threadname() << " start ..." << std::endl;}
}

run() 函数遍历线程池中的所有线程,并为每个线程创建一个 ThreadData<T> 类型的对象 td。ThreadData<T> 类型是一个模板类,用于封装线程池和线程的关系,保存了线程池的指针和线程的名称

定义了 ThreadPool 类的成员函数 push(),它的作用是向线程池中添加一个任务。
void push(const T &in)
{LockGuard lockguard(&_mutex); // 使用 LockGuard 对象对互斥量进行加锁 RALL设计_task_queue.push(in);         // 进队列push// 使用 pthread_cond_signal() 函数发送一个条件信号,以通知等待在条件变量 _cond 上的线程有新的任务可以处理。pthread_cond_signal(&_cond);
}
作用是获取 ThreadPool 类的单例对象。
static ThreadPool<T> *getInstance()
{if (nullptr == tp){_singlock.lock();if (nullptr == tp){tp = new ThreadPool<T>();}_singlock.unlock();}return tp;
}

getInstance()函数首先检查静态成员变量 tp 是否为空指针。如果 tp 不为空,直接返回 tp指向的对象。否则,它使用单例模式的方式创建一个新的 ThreadPool 对象,并将其赋值给 tp。在创建对象时,使用了双重检查锁定的方式来确保线程安全。最后,getInstance() 函数返回 tp 指向的对象。

调用 getInstance() 函数,可以获取 ThreadPool 类的单例对象。这使得线程池可以全局共享,并确保线程池的唯一性。通过使用单例模式,可以简化线程池的实现,提高代码的可维护性和可扩展性。


展示

main.cc

#include "ThreadPool.hpp"
#include "Task.hpp"
#include <memory>
#include <unistd.h>int main()
{// 调用了 ThreadPool<Task> 类的静态成员函数 getInstance() 和成员函数 run(),// 用于获取 ThreadPool 类的单例对象并运行线程池// getInstance() 函数返回一个指向 ThreadPool<Task> 类对象的指针,// 该对象是全局唯一的,并使用了双重检查锁定的方式确保线程安全。然后,调用该对象的成员函数 run(),用于启动线程池的运行ThreadPool<Task>::getInstance()->run();int x, y;char op;while (1){std::cout << "请输入数据1# ";std::cin >> x;std::cout << "请输入数据2# ";std::cin >> y;std::cout << "请输入你要进行的运算#";std::cin >> op;Task t(x, y, op, mymath);ThreadPool<Task>::getInstance()->push(t);sleep(1);}
}

在这里插入图片描述


如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀

相关文章:

【Linux】简单线程池的设计与实现 -- 单例模式

前言对锁的封装整体代码LockGuard - RALLRALLMutex封装 对线程创建的封装整体代码成员函数解释声明 业务处理封装-加减乘除&#xff08;可有可无&#xff09;整体代码成员函数解释声明 线程池的设计与实现整体代码成员函数解释声明 展示 前言 线程池: 一种线程使用模式。线程过…...

[RoarCTF 2019Online Proxy]sql巧妙盲注

文章目录 [RoarCTF 2019Online Proxy]sql巧妙盲注解题脚本脚本解析 [RoarCTF 2019Online Proxy]sql巧妙盲注 解题 在源代码界面发现&#xff1a;Current Ip 我们会联想到&#xff1a;X-Forwarded-For来修改ip&#xff1a; 结果我们发现&#xff0c;response会讲Last Ip回显出…...

flutter开发实战-just_audio实现播放音频暂停音频设置音量等

flutter开发实战-just_audio实现播放音频暂停音频设置音量等 最近开发过程中遇到需要播放背景音等音频播放&#xff0c;这里使用just_audio来实现播放音频暂停音频设置音量等 一、引入just_audio 在pubspec.yaml引入just_audio just_audio: ^2.7.0在iOS上&#xff0c;video_p…...

【Bert101】最先进的 NLP 模型解释【01/4】

0 什么是伯特&#xff1f; BERT是来自【Bidirectional Encoder Representations from Transformers】变压器的双向编码器表示的缩写&#xff0c;是用于自然语言处理的机器学习&#xff08;ML&#xff09;模型。它由Google AI Language的研究人员于2018年开发&#xff0c;可作为…...

c语言经典例题讲解(输出菱形,喝汽水问题)

目录 一、输出菱形 二、喝汽水问题 方法1&#xff1a;一步一步来 方法二&#xff1a;直接套公式 一、输出菱形 输出类似于下图的菱形&#xff1a; 通过分析&#xff1a;1、先分为上下两部分输出 2.在输出前先输出空格 3.找规律进行输出 可知&#xff0c;可令上半部分lin…...

【Flutter】【基础】CustomPaint 绘画功能(一)

功能&#xff1a;CustomPaint 相当于在一个画布上面画画&#xff0c;可以自己绘制不同的颜色形状等 在各种widget 或者是插件不能满足到需求的时候&#xff0c;可以自己定义一些形状 使用实例和代码&#xff1a; CustomPaint&#xff1a; 能使你绘制的东西显示在你的ui 上面&a…...

iOS 实现图片高斯模糊效果

效果图 用到了 UIVisualEffectView 实现代码 - (UIVisualEffectView *)bgEffectView{if(!_bgEffectView){UIBlurEffect *blur [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];_bgEffectView [[UIVisualEffectView alloc] initWithEffect:blur];}return _bgEffect…...

[保研/考研机试] KY7 质因数的个数 清华大学复试上机题 C++实现

描述 求正整数N(N>1)的质因数的个数。 相同的质因数需要重复计算。如1202*2*2*3*5&#xff0c;共有5个质因数。 输入描述&#xff1a; 可能有多组测试数据&#xff0c;每组测试数据的输入是一个正整数N&#xff0c;(1<N<10^9)。 输出描述&#xff1a; 对于每组数…...

初识Redis

目录 认识Redis分布式系统Redis的特性Redis的应用场景Redis客户端Redis命令 认识Redis 上面一段话是官网给出的对Redis的介绍&#xff0c;in-memory data store表明Redis是在内存中存储数据的&#xff0c;这和我们接触的其他数据库就有很大的不同&#xff0c;比如MySQL&#xf…...

每天一道leetcode:115. 不同的子序列(动态规划困难)

今日份题目&#xff1a; 给你两个字符串 s 和 t &#xff0c;统计并返回在 s 的 子序列 中 t 出现的个数。 题目数据保证答案符合 32 位带符号整数范围。 示例1 输入&#xff1a;s "rabbbit", t "rabbit" 输出&#xff1a;3 解释&#xff1a; 如下所…...

服务器数据恢复-RAID5多块磁盘离线导致崩溃的数据恢复案例

服务器数据恢复环境&#xff1a; DELL POWEREDGE某型号服务器中有一组由6块SCSI硬盘组建的RAID5阵列&#xff0c;LINUX REDHAT操作系统&#xff0c;EXT3文件系统&#xff0c;存放图片文件。 服务器故障&分析&#xff1a; 服务器raid5阵列中有一块硬盘离线&#xff0c;管理员…...

NO.2 MyBatis框架:创建Mapper接口和映射文件,实现基本增删改查

目录 1、Mapper接口和映射文件关系 2、Mapper接口和映射文件的命名规则 2.1 Mapper接口的命名规则 2.2 映射文件的命名规则 3、Mapper接口和映射文件的创建及增删改查的实现 3.1 Mapper接口和映射文件的创建 3.2 增删改查的实现 3.2.1表结构 3.2.2 创建表User对应的实…...

【JS】怎么提取object类的内容

需求&#xff1a;在网页端中通过getElementsByClassName获取到一个元素&#xff0c;想提取其中的数字内容做个if判断&#xff0c;奈何一直提取不了 开始获取元素时&#xff0c;以为默认就是字符类型&#xff1b;但使用操作字符的函数就失败&#xff0c;然后就考虑数据类型是不是…...

分布式系统的 38 个知识点

天天说分布式分布式&#xff0c;那么我们是否知道什么是分布式&#xff0c;分布式会遇到什么问题&#xff0c;有哪些理论支撑&#xff0c;有哪些经典的应对方案&#xff0c;业界是如何设计并保证分布式系统的高可用呢&#xff1f; 1. 架构设计 这一节将从一些经典的开源系统架…...

机器学习基础(二)

线性回归 误差是独立并且具有相同的分布通常认为服从均值为0方差为的高斯分布。 损失函数(loss Function)/代价函数(Cost Function) 其实两种叫法都可以,损失函数(loss function)或代价函数(cost function)是将随机事件或其有关随机变量的取值映射为非负实数以表示该随…...

Java 实现Rtsp 转rtmp,hls,flv

服务支撑&#xff1a;FFmpeg srs(流媒体服务器) 整个流程是 FFmpeg 收流转码 推 rtmp 到流媒体服务 流媒体服务再 分发流到公网 搭建流媒体服务: 1. SRS (Simple Realtime Server) | SRS &#xff08;本例子使用的是SrS 安装使用docker &#xff09; 2.GitHub - ZLMedi…...

机器学习基础(三)

逻辑回归 场景 垃圾邮件分类 预测肿瘤是良性还是恶性 预测某人的信用是否良好 正确率与召回率 正确率与召回率(Precision & Recall)是广泛应用于信息检索和统计学分类领域的两个度量值,用来评价结果的质量。 一般来说,正确率就是检索出来的条目有多少是正确的,召回率就…...

Kubeadm安装K8s集群

一、硬件环境 准备3台Linux服务器&#xff0c;此处用Vmware虚拟机。 主机名CPU内存k8smaster2核4Gk8snode12核4Gk8snode22核4G 二、系统前置准备 配置三台主机的hosts文件 cat << EOF > /etc/hosts 192.168.240.130 k8smaster 192.168.240.132 k8snode1 192.168.…...

【C++】开源:spdlog跨平台日志库配置使用

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍spdlog日志库配置使用。 无专精则不能成&#xff0c;无涉猎则不能通。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下…...

[Azkaban] No active executors found

没有找到活动的executors&#xff0c;需在MySQL数据库里设置端口为12321的executors表的active为1&#xff1a; select * from executors;如果显示active0 则需要进行处理&#xff1a; update azkaban.executors set active1;当active0&#xff0c;更新为1时&#xff0c;用 n…...

STM32+rt-thread判断是否联网

一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...

C# SqlSugar:依赖注入与仓储模式实践

C# SqlSugar&#xff1a;依赖注入与仓储模式实践 在 C# 的应用开发中&#xff0c;数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护&#xff0c;许多开发者会选择成熟的 ORM&#xff08;对象关系映射&#xff09;框架&#xff0c;SqlSugar 就是其中备受…...

html-<abbr> 缩写或首字母缩略词

定义与作用 <abbr> 标签用于表示缩写或首字母缩略词&#xff0c;它可以帮助用户更好地理解缩写的含义&#xff0c;尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时&#xff0c;会显示一个提示框。 示例&#x…...

scikit-learn机器学习

# 同时添加如下代码, 这样每次环境(kernel)启动的时候只要运行下方代码即可: # Also add the following code, # so that every time the environment (kernel) starts, # just run the following code: import sys sys.path.append(/home/aistudio/external-libraries)机…...

并发编程 - go版

1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程&#xff0c;系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...

通过MicroSip配置自己的freeswitch服务器进行调试记录

之前用docker安装的freeswitch的&#xff0c;启动是正常的&#xff0c; 但用下面的Microsip连接不上 主要原因有可能一下几个 1、通过下面命令可以看 [rootlocalhost default]# docker exec -it freeswitch fs_cli -x "sofia status profile internal"Name …...

云原生周刊:k0s 成为 CNCF 沙箱项目

开源项目推荐 HAMi HAMi&#xff08;原名 k8s‑vGPU‑scheduler&#xff09;是一款 CNCF Sandbox 级别的开源 K8s 中间件&#xff0c;通过虚拟化 GPU/NPU 等异构设备并支持内存、计算核心时间片隔离及共享调度&#xff0c;为容器提供统一接口&#xff0c;实现细粒度资源配额…...

02.运算符

目录 什么是运算符 算术运算符 1.基本四则运算符 2.增量运算符 3.自增/自减运算符 关系运算符 逻辑运算符 &&&#xff1a;逻辑与 ||&#xff1a;逻辑或 &#xff01;&#xff1a;逻辑非 短路求值 位运算符 按位与&&#xff1a; 按位或 | 按位取反~ …...

STL 2迭代器

文章目录 1.迭代器2.输入迭代器3.输出迭代器1.插入迭代器 4.前向迭代器5.双向迭代器6.随机访问迭代器7.不同容器返回的迭代器类型1.输入 / 输出迭代器2.前向迭代器3.双向迭代器4.随机访问迭代器5.特殊迭代器适配器6.为什么 unordered_set 只提供前向迭代器&#xff1f; 1.迭代器…...

IP选择注意事项

IP选择注意事项 MTP、FTP、EFUSE、EMEMORY选择时&#xff0c;需要考虑以下参数&#xff0c;然后确定后选择IP。 容量工作电压范围温度范围擦除、烧写速度/耗时读取所有bit的时间待机功耗擦写、烧写功耗面积所需要的mask layer...