[linux] Implementation of thread pool based on singleton mode

1. Thread pool

1.1 The concept of pooling

When we process tasks, we usually create a thread to process a task . The first problem here is that efficiency will be reduced (creating threads requires cost).

Next, we can compare the expansion mechanism of STL. When we use vector to apply for expansion, even if we only apply for one more space, it will give us 1.5 times or two times the expansion directly according to the expansion mechanism, so that when we later If you still want to expand the capacity, you don't need to apply for resources.

And we can use this idea, we first create a batch of threads , when there are no tasks in the task queue, each thread sleeps first, once the task queue comes to the task, it will wake up the thread to process.
The cost of waking up a thread is less than the cost of creating a thread.

And we call this model the thread pool .

1.2 Implementation of thread pool

First introduce the components encapsulated in the native thread library, and then make some small adjustments.

// mythread.hpp
#pragma once

#include <iostream>
#include <pthread.h>
#include <cstring>
#include <string>
#include <cassert>
#include <functional>
#include <unistd.h>

class Thread
{
    
    
    typedef std::function<void*(void*)> func_t;
private:
    // 不加static就会有this指针
    static void* start_routine(void* args)
    {
    
    
        //return _func(args);
        // 无this指针,无法调用
        Thread* pct = static_cast<Thread*>(args);
        pct->_func(pct->_args);
        return nullptr;
    }
public:
    Thread(func_t fun, void* args = nullptr)
        : _func(fun)
        , _args(args)
    {
    
    
        char buf[64];
        snprintf(buf, sizeof buf, "thread-%d", _number++);
        _name = buf;
    }

    void start()
    {
    
    
        // int n = pthread_create(&_tid, nullptr, _func, _args);
        // _func是C++函数,pthread_create是C接口,不能混编
        int n = pthread_create(&_tid, nullptr, start_routine, this);
        assert(n == 0);
        (void)n;
    }

    std::string GetName()
    {
    
    
        return _name;
    }

    void join()
    {
    
    
        int n = pthread_join(_tid, nullptr);
        assert(n == 0);
        (void)n;
    }
private:
    std::string _name;// 线程名
    pthread_t _tid;// 线程id
    func_t _func;// 调用方法
    void *_args;// 参数
    static int _number;// 线程编号
};

int Thread::_number = 1;


// Main.cc
#include "mythread.hpp"

using std::cout;
using std::endl;

void* threadhandler(void* args)
{
    
    
    std::string ret = static_cast<const char*>(args);
    while(true)
    {
    
    
        cout << ret << endl;
        sleep(1);
    }
}

int main()
{
    
    
    Thread t1(threadhandler, (void*)"thead1");
    Thread t2(threadhandler, (void*)"thead2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    return 0;
}

insert image description here

After verifying that there is no problem, the thread pool can create a batch of threads.

Since multiple threads are required to access the task queue, locks are needed to protect resources, and we can directly introduce the lock widget we wrote before:

// mymutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>

class Mutex 
{
    
    
public:
    Mutex(pthread_mutex_t* plock = nullptr)
        : _plock(plock)
    {
    
    }

    void lock()
    {
    
    
        // 被设置过
        if(_plock)
        {
    
    
            pthread_mutex_lock(_plock);
        }
    }

    void unlock()
    {
    
    
        if(_plock)
        {
    
    
            pthread_mutex_unlock(_plock);
        }
    }
private:
    pthread_mutex_t *_plock;
};

// 自动加锁解锁
class LockAuto
{
    
    
public:
    LockAuto(pthread_mutex_t *plock)
        : _mutex(plock)
    {
    
    
        _mutex.lock();
    }

    ~LockAuto()
    {
    
    
        _mutex.unlock();
    }
private:
    Mutex _mutex;
};

When creating a batch of threads, we need to implement the running function of the thread, because it is passed to the _func in the Thread class, so there cannot be a this pointer, and it must be a static member function . But when it is set as a static member function, there is no this pointer, and member variables (locks and task queues, etc.) cannot be accessed, so we need to encapsulate these interfaces.

template <class T>
class ThreadPool
{
    
    
private:
    static void* handlerTask(void* args)
    {
    
    
        ThreadPool<T>* tp = static_cast<ThreadPool<T>*>(args);
        while(true)
        {
    
    
            tp->lockqueue();
            while(tp->isqueueempty())
            {
    
    
                tp->threadwait();
            }
            T t = tp->pop();
            tp->unlockqueue();
            t();// 处理任务
        }
    }

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

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

    bool isqueueempty()
    {
    
    
        return _tasks.empty();
    }

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

    T pop()
    {
    
    
        T res = _tasks.front();
        _tasks.pop();
        return res;
    }
public:
    ThreadPool(int num = 5)
        : _num(num)
    {
    
    
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        // 创建线程
        for(int i = 0; i < _num; i++)
        {
    
    
            _threads.push_back(new Thread(handlerTask, this));
        }
    }

    void start()
    {
    
    
        for(auto& t : _threads)
        {
    
    
            t->start();
            cout << t->GetName() << " start..." << endl;
        }
    }

    void push(const T& in)
    {
    
    
        LockAuto lock(&_mutex);
        _tasks.push(in);
        // 唤醒池中的一个线程
        pthread_cond_signal(&_cond);
    }

    ~ThreadPool()
    {
    
    
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for(auto & e : _threads)
        {
    
    
            delete e;
        }
    }
private:
    int _num;// 线程数量
    std::vector<Thread*> _threads;
    std::queue<T> _tasks;// 任务队列
    pthread_mutex_t _mutex;// 保护任务队列
    pthread_cond_t _cond;
};

Some details:

1️⃣ When constructing a function, the thread needs to pass parameters. In order to allow the thread function (static member function) to obtain member variables, the this object must be passed to it.
2️⃣ In the thread function, the processing task t()should be placed after unlocking, because pop()the essence is to get the task from the public resource into the independent stack structure of the current process . First, it does not need to be protected. Secondly, if it is placed in the lock and I don't know how long it will run between unlocks, resulting in a waste of resources. The current situation is that the thread releases the lock after getting the task, and processes the task by itself without affecting other threads to take the task.

Now we want to process the calculation of various data as before, so first introduce the task component:

// Task.hpp
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <cstdio>
#include <unordered_map>

class Task
{
    
    
    typedef std::function<int(int, int, char)> func_t;
public:
    Task()
    {
    
    }

    Task(int x, int y, char op, func_t func)
        : _x(x)
        , _y(y)
        , _op(op)
        , _func(func)
    {
    
    }

    std::string operator()()
    {
    
    
        int res = _func(_x, _y, _op);
        char buf[64];
        snprintf(buf, sizeof buf, "%d %c %d = %d", _x, _op, _y, res);
        return buf;
    }
    std::string tostringTask()
    {
    
    
        char buf[64];
        snprintf(buf, sizeof buf, "%d %c %d = ?", _x, _op, _y);
        return buf;
    }
private:
    int _x;
    int _y;
    char _op; 
    func_t _func;
};

const std::string oper = "+-*/";

std::unordered_map<char, std::function<int(int, int)>> hash = {
    
    
        {
    
    '+', [](int x, int y)->int{
    
    return x + y;}},
        {
    
    '-', [](int x, int y)->int{
    
    return x - y;}},
        {
    
    '*', [](int x, int y)->int{
    
    return x * y;}},
        {
    
    '/', [](int x, int y)->int{
    
    
            if(y == 0)
            {
    
    
                std::cerr << "除0错误" << endl;
                return -1;
            }
            return x / y;}},
    };

int myMath(int x, int y, char op)
{
    
    
    int res = hash[op](x, y);
    return res;
}

Now we want to know which thread is processing the task when the thread is processing it. We can change the position of the parameter passed in, not at the time of construction, but at runtime, so that it can be passed into the thread to start function.
insert image description here

insert image description here
Implementation code:

// ThreadPool.hpp
#pragma once

#include <vector>
#include <queue>
#include "mythread.hpp"
#include "mymutex.hpp"
#include "Task.hpp"

using std::cout;
using std::endl;

const int N = 5;

template <class T>
class ThreadPool;

template <class T>
struct ThreadData
{
    
    
    ThreadPool<T>* _tp;
    std::string _name;
    ThreadData(ThreadPool<T>* tp, const std::string& name)
        : _tp(tp)
        , _name(name)
    {
    
    }
};

template <class T>
class ThreadPool
{
    
    
private:
    static void* handlerTask(void* args)
    {
    
    
        ThreadData<T>* tdp = static_cast<ThreadData<T>*>(args);
        while(true)
        {
    
    
            tdp->_tp->lockqueue();
            while(tdp->_tp->isqueueempty())
            {
    
    
                tdp->_tp->threadwait();
            }
            T t = tdp->_tp->pop();
            tdp->_tp->unlockqueue();
            cout << tdp->_name << " 获取任务: " << t.tostringTask() << " 结果是: " << t() << endl;
            //t();// 处理任务
        }
    }

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

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

    bool isqueueempty()
    {
    
    
        return _tasks.empty();
    }

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

    T pop()
    {
    
    
        T res = _tasks.front();
        _tasks.pop();
        return res;
    }
public:
    ThreadPool(int num = 5)
        : _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 start()
    {
    
    
        for(auto& t : _threads)
        {
    
    
            ThreadData<T>* td = new ThreadData<T>(this, t->GetName());
            t->start(handlerTask, td);
        }
    }

    void push(const T& in)
    {
    
    
        LockAuto lock(&_mutex);
        _tasks.push(in);
        // 唤醒池中的一个线程
        pthread_cond_signal(&_cond);
    }

    ~ThreadPool()
    {
    
    
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for(auto & e : _threads)
        {
    
    
            delete e;
        }
    }
private:
    int _num;// 线程数量
    std::vector<Thread*> _threads;
    std::queue<T> _tasks;// 任务队列
    pthread_mutex_t _mutex;// 保护任务队列
    pthread_cond_t _cond;
};

// Main.cc
#include "mythread.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <cstdlib>

int main()
{
    
    
    ThreadPool<Task>* tp = new ThreadPool<Task>();
    tp->start();
    srand(time(0));
    int x, y;
    char op;
    while(true)
    {
    
    
        x = rand() % 100;
        y = rand() % 50;
        op = oper[rand() % 4];
        Task t(x, y, op, myMath);
        tp->push(t);
        sleep(1);
    }
    return 0;
}

insert image description here

1.3 Usage Scenarios of Thread Pool

1️⃣ A large number of threads are required to complete the task, and the time to complete the task is relatively short. It is very appropriate for the WEB server to complete tasks such as web page requests using thread pool technology. Because a single task is small, and the number of tasks is huge, you can imagine the number of hits on a popular website. But for long-term tasks, such as a Telnet connection request, the advantages of the thread pool are not obvious. Because the Telnet session time is much longer than the thread creation time.
2️⃣Applications with demanding performance requirements, such as requiring the server to respond quickly to client requests.
3️⃣An application that accepts a large number of sudden requests, but does not cause the server to generate a large number of threads. A large number of sudden customer requests will generate a large number of threads without a thread pool. Although the maximum number of threads in most operating systems is not a problem in theory, generating a large number of threads in a short period of time may cause the memory to reach the limit and cause errors.

2. Singleton mode

2.1 Concept of singleton mode

The singleton pattern has been introduced in detail in the previous article [C++] Special Class Design (Singleton Pattern) .
Here is a summary in one sentence: the characteristic of the singleton mode is that there is only one unique object in the world.
The implemented modes include hungry man mode and lazy man mode.

Hungry man means that the program loads the object as soon as it starts running, while lazy man loads the object when it is needed.

for example:

We usually use malloc/new as a lazy mode. When we apply, we don’t actually apply for it in physical memory, but when we want to write, we apply for physical memory and establish a mapping relationship between virtual address space and physical space (page surface).

So the core idea of ​​the lazy man mode is delayed loading .

2.2 Thread pool in singleton mode

The first step we have to do is to make the constructor private , and then overload the copy construction and assignment operator delete.
insert image description here
The next step is to define a static pointer in the member variable to facilitate the acquisition of singleton objects.
insert image description here
When setting the function to obtain the singleton object, pay attention to setting it as a static member function, because there is no object at all before the object is obtained, and the non-static member function (no this pointer) cannot be called.

static ThreadPool<T>* GetSingle()
{
    
    
    if(_tp == nullptr)
    {
    
    
        _tp = new ThreadPool<T>();
    }
    return _tp;
}

When calling:

int main()
{
    
    
    ThreadPool<Task>::GetSingle()->start();
    srand(time(0));
    int x, y;
    char op;
    while(true)
    {
    
    
        x = rand() % 100;
        y = rand() % 50;
        op = oper[rand() % 4];
        Task t(x, y, op, myMath);
        ThreadPool<Task>::GetSingle()->push(t);
        sleep(1);
    }
    return 0;
}

Running results:
insert image description here
However, there may be scenarios where multiple threads apply for resources at the same time, so a lock is needed to protect this resource, and this lock must also be set to static, because the GetSingle()function is static and cannot access member functions.

insert image description here

static ThreadPool<T>* GetSingle()
{
    
    
    _singlelock.lock();
    if(_tp == nullptr)
    {
    
    
        _tp = new ThreadPool<T>();
    }
    _singlelock.unlock();
    return _tp;
}

operation result:
insert image description here

2.3 Anti-over-optimization

When we have multiple threads, it is likely to put the pointer of the resource into the register. If a thread modifies it, then there will be problems.
So we need to use the volatile keyword to ensure memory visibility.

volatile static ThreadPool<T>* _tp

3. Source code

// ThreadPool.hpp
#pragma once

#include <vector>
#include <queue>
#include <mutex>
#include "mythread.hpp"
#include "mymutex.hpp"
#include "Task.hpp"

using std::cout;
using std::endl;

const int N = 5;

template <class T>
class ThreadPool;

template <class T>
struct ThreadData
{
    
    
    ThreadPool<T>* _tp;
    std::string _name;
    ThreadData(ThreadPool<T>* tp, const std::string& name)
        : _tp(tp)
        , _name(name)
    {
    
    }
};

template <class T>
class ThreadPool
{
    
    
private:
    static void* handlerTask(void* args)
    {
    
    
        ThreadData<T>* tdp = static_cast<ThreadData<T>*>(args);
        while(true)
        {
    
    
            tdp->_tp->lockqueue();
            while(tdp->_tp->isqueueempty())
            {
    
    
                tdp->_tp->threadwait();
            }
            T t = tdp->_tp->pop();
            tdp->_tp->unlockqueue();
            cout << tdp->_name << " 获取任务: " << t.tostringTask() << " 结果是: " << t() << endl;
            //t();// 处理任务
        }
    }

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

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

    bool isqueueempty()
    {
    
    
        return _tasks.empty();
    }

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

    T pop()
    {
    
    
        T res = _tasks.front();
        _tasks.pop();
        return res;
    }

    ThreadPool(int num = 5)
        : _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(const ThreadPool<T>& ) = delete;
    ThreadPool<T> operator=(const ThreadPool<T>&) = delete;

public:

    void start()
    {
    
    
        for(auto& t : _threads)
        {
    
    
            ThreadData<T>* td = new ThreadData<T>(this, t->GetName());
            t->start(handlerTask, td);
        }
    }

    void push(const T& in)
    {
    
    
        LockAuto lock(&_mutex);
        _tasks.push(in);
        // 唤醒池中的一个线程
        pthread_cond_signal(&_cond);
    }

    ~ThreadPool()
    {
    
    
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for(auto & e : _threads)
        {
    
    
            delete e;
        }
    }

    static ThreadPool<T>* GetSingle()
    {
    
    
        if(_tp == nullptr)
        {
    
    
            _singlelock.lock();
            if(_tp == nullptr)
            {
    
    
                _tp = new ThreadPool<T>();
            }
            _singlelock.unlock();
        }
        return _tp;
    }

private:
    int _num;// 线程数量
    std::vector<Thread*> _threads;
    std::queue<T> _tasks;// 任务队列
    pthread_mutex_t _mutex;// 保护任务队列
    pthread_cond_t _cond;
    volatile static ThreadPool<T>* _tp;
    static std::mutex _singlelock;
};

template <class T>
ThreadPool<T>* ThreadPool<T>::_tp = nullptr;

template <class T>
std::mutex ThreadPool<T>::_singlelock;


Guess you like

Origin blog.csdn.net/qq_66314292/article/details/130273721