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

这篇具有很好参考价值的文章主要介绍了【Linux】简单线程池的设计与实现 -- 单例模式。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

线程池:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络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_;    // 线程的 ID
        static 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); // TODO
    assert(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;
        }
        else
            result = x / y;
    }
    break;
    case '%':
    {
        if (y == 0)
        {
            std::cerr << "mod zero error!" << std::endl;
            result = -1;
        }
        else
            result = x % y;
    }
    break;
    default:
        // do nothing
        break;
    }

    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】简单线程池的设计与实现 -- 单例模式,Linux系统基础,linux,单例模式,运维,后端,线程池


如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀文章来源地址https://www.toymoban.com/news/detail-652674.html

到了这里,关于【Linux】简单线程池的设计与实现 -- 单例模式的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Linux学习】多线程——线程池 | 单例模式

    🐱作者:一只大喵咪1201 🐱专栏:《Linux学习》 🔥格言: 你只管努力,剩下的交给时间! 多线程部分的知识讲解到此就告一段落了,现在创建一个线程池来检验一下我们的学习成果。 一种线程使用模式,线程过多会带来调度开销,进而影响缓存局部性和整体性能。 线程池

    2024年02月09日
    浏览(49)
  • 【Linux】线程终结篇:线程池以及线程池的实现

    linux线程完结 文章目录 前言 一、线程池的实现 二、了解性知识 1.其他常见的各种锁 2.读者写者问题 总结 什么是线程池呢? 线程池一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行

    2024年02月12日
    浏览(35)
  • 设计模式3:单例模式:静态内部类模式是怎么保证单例且线程安全的?

    上篇文章:设计模式3:单例模式:静态内部类单例模式简单测试了静态内部类单例模式,确实只生成了一个实例。我们继续深入理解。 静态变量什么时候被初始化? 这行代码 private static Manager instance = new Manager(); 什么时候执行? 编译期间将.java文件转为.class文件,运行期间

    2024年02月12日
    浏览(46)
  • 【Linux】多线程之单例模式

    设计模式就是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。 创建模式中: 抽象工厂模式 ,提供一个创建一系列相关或相互依赖对象的接口,而无需指定

    2024年02月13日
    浏览(41)
  • Linux之 线程池 | 单例模式的线程安全问题 | 其他锁

    目录 一、线程池 1、线程池 2、线程池代码 3、线程池的应用场景 二、单例模式的线程安全问题 1、线程池的单例模式 2、线程安全问题 三、其他锁 线程池是一种线程使用模式。线程池里面可以维护一些线程。 为什么要有线程池? 因为在我们使用线程去处理各种任务的时候,

    2024年04月18日
    浏览(42)
  • Liunx下的消费者与生产者模型与简单线程池的实现

    本文主要会结束消费者生产者模型,以及简单线程池的实现。 之前我们学了条件变量和互斥等概念。条件变量本质就是一个队列,它会将因为某种条件不满足不能往后执行的线程添加到这个队列中,避免线程做无用功,当条件满足时,会将队列中的线程重新唤醒继续执行。我

    2024年02月13日
    浏览(36)
  • 【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式

    🍎 个人博客: 个人主页 🏆 个人专栏: JAVA ⛳️   功不唐捐,玉汝于成 目录 前言 正文 懒汉式(Lazy Initialization): 双重检查锁定(Double-Checked Locking): 结语 我的其他博客 在软件设计中,单例设计模式是一种重要的设计思想,它确保了一个类只有一个实例,并提供了一

    2024年01月15日
    浏览(55)
  • 【Java中23种设计模式-单例模式2--懒汉式2线程安全】

    加油,新时代打工人! 简单粗暴,学习Java设计模式。 23种设计模式定义介绍 Java中23种设计模式-单例模式 Java中23种设计模式-单例模式2–懒汉式线程不安全 通过运行结果看,两个线程的地址值是相同的,说明内存空间里,创建了一个对象。

    2024年02月20日
    浏览(46)
  • 【Java|多线程与高并发】设计模式-单例模式(饿汉式,懒汉式和静态内部类)

    设计模式是一种在软件开发中常用的解决复杂问题的方法论。它提供了一套经过验证的解决方案,用于解决特定类型问题的设计和实现。设计模式可以帮助开发人员提高代码的可重用性、可维护性和可扩展性。 设计模式有很多,本文主要介绍单例模式. 单例模式是一种创建型设

    2024年02月11日
    浏览(55)
  • Java 枚举实现单例模式,线程安全又优雅!

    这种DCL写法的优点:不仅线程安全,而且延迟加载。 1.1 为什么要double check?去掉第二次check行不行? 当然不行,当2个线程同时执行getInstance方法时,都会执行第一个if判断,由于锁机制的存在,会有一个线程先进入同步语句,而另一个线程等待,当第一个线程执行了 new Sin

    2024年02月02日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包