[Linux] thread pool|singleton mode|STL, smart pointer thread safety|reader writer problem

Thread Pool

When we go to process tasks, one task corresponds to one thread for processing, and the efficiency is relatively low. We can create a batch of threads in advance. When there are no tasks in the task queue, each thread will sleep. When there are tasks in the queue, they can be processed by circular threads. The cost of waking up a thread is smaller than the cost of creating an entire thread, which is the logical idea of ​​the thread pool.

线程池: A thread usage mode. Too many threads introduce scheduling overhead, which affects cache locality and overall performance. The thread pool maintains multiple threads, waiting for the supervisor to assign tasks that can be executed concurrently. This avoids the cost of creating and destroying threads when processing short-lived tasks. The thread pool can not only ensure the full utilization of the core, but also prevent excessive scheduling. The number of available threads should depend on the number of concurrent processors, processor cores, memory, network sockets, etc. available.

Common application scenarios of thread pool are as follows:

A large number of threads are required to complete the task, and the time to complete the task is relatively short.

Performance-critical applications, such as requiring the server to respond quickly to client requests.

An application that accepts a large number of sudden requests, but does not cause the server to generate a large number of threads.

thread pool code

Overall frame structure:

image-20230424194947899

Thread.hpp 's simple package thread, let's do a simple verification below

The main member variables of the Thread class are the thread name, function, thread parameters, parameter ID and corresponding number.
The Thread class provides a no-argument structure to complete the assignment of the member variable name.

At the same time, it mainly provides the start interface and the join interface externally. For the join interface, it is the thread waiting, and for the start interface, it is the interface for creating threads. If it is called externally, we need to pass in the corresponding function and the parameters corresponding to the thread.

#pragma once
#include <pthread.h>
#include <iostream>
#include <cassert>
#include <string>
#include <functional>
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()
        {}

        void* callback()
        {
            return func_(args_);
        }
    private:
        std::string name_;
        func_t func_;
        void *args_;
        pthread_t tid_;
        static int threadnum;
    };
    int Thread::threadnum = 1;
}
//进行调用
using namespace ThreadNs;
void* handler(void*args)
{
    string ret = static_cast<const char*>(args);
    while(true)
    {
        cout<<ret<<endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    Thread t1;
    Thread t2;
    t1.start(handler,(void*)"thread1");
    t2.start(handler,(void*)"thread2");
    t1.join();
    t1.join();
    return 0;
}

image-20230424203114255

For the task queue, which can be accessed by multiple threads, we need to add lock protection, and introduce the lock widget written before:

LockGuard.hpp

#include <iostream>
#include <mutex>
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_;
};

The thread pool code is as follows: When creating a batch of threads, we need to implement the thread running function static void*handlerTask. The reason why it is static is that we need to pass this running function to func_ in the Thread class, and there cannot be this pointer. So it's a static member function. Without this pointer, we cannot access the member variables in ThreadPool, so we need to encapsulate the interface for it to call.

ThreadPool.hpp

#pragma once
#include "Thread.hpp"
#include "LockGuard.hpp"
#include <vector>
#include <queue>
#include <pthread.h>
#include <mutex>
#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;
        ThreadPool<T> *threadpool = static_cast<ThreadPool<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;
        return nullptr;
    }
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;
    }
    
    void Push(const T &in)
    {
        LockGuard lockguard(&_mutex);
        _task_queue.push(in);
        pthread_cond_signal(&_cond);
    }

    pthread_mutex_t *mutex()
    {
        return &_mutex;
    }

public:
    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 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;
        }
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for (const auto &t : _threads)
            delete t;
    }
private:
    int _num;
    std::vector<Thread *> _threads;
    std::queue<T> _task_queue;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
};

We have templated the thread pool, so the types of tasks stored in the thread pool can be arbitrary; now we imagine the calculation of processing various data before, so first introduce the task component Task.hpp:

Task.hpp

#pragma once
#include <iostream>
#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:
        break;
    }
    return result;
}

main.cc

#include "ThreadPool.hpp"
#include "Thread.hpp"
#include "Task.hpp"
#include <unistd.h>
#include <ctime>
int main()
{
   ThreadPool<Task>* tp = new ThreadPool<Task>();
   tp->run();
   srand(time(0));
   int x,y;
   char op;
   while(true)
   {
    x = rand()%10+1;
    y = rand()%20+1;
    op  =oper[rand()%oper.size()];
    Task t(x,y,op,mymath);
    tp->Push(t);
    sleep(1);
   }
    return 0;
}

image-20230425171937402

Thread pool singleton pattern

The singleton pattern is a creational design pattern that ensures that only one instance of a class exists and provides a global access point to access that instance. The Hungry Way and the Lazy Way are two common implementations of the Singleton pattern. The main characteristics of the singleton pattern include:

There can only be one instance.
A global access point for easy access to the instance.
In many server development scenarios, it is often necessary for the server to load a lot of data (hundreds of gigabytes) into the memory. At this time, it is often necessary to use a singleton class to manage these data.

The first step we have to do is to make the constructor private , and then overload the copy construction and assignment operator delete:

image-20230425184056441

The next step is to define a static pointer in the member variable to facilitate the acquisition of singleton objects. :

image-20230425183045650

When setting the function to obtain a singleton object, pay attention to setting it as a static member function, because there is no object at all before obtaining the object, and it is impossible to call a non-static member function (no this pointer):

image-20230425183113446

The main function calls:

image-20230425183858039

image-20230425184131497

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 function is GetSingle()static

image-20230426000144946

operation result:

image-20230426000321053

The complete code of thread pool singleton mode:

#pragma once
#include "Thread.hpp"
#include "LockGuard.hpp"
#include <vector>
#include <queue>
#include <pthread.h>
#include <mutex>
#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;
        ThreadPool<T> *threadpool = static_cast<ThreadPool<T> *>(args);
        while (true)
        {
            T t;
            {
                // td->threadpool->lockQueue();
                LockGuard lockguard(td->threadpool->mutex());

                while (td->threadpool->isQueueEmpty())
                {
                    td->threadpool->threadWait();
                }
                t = td->threadpool->pop(); // pop的本质是将任务从公共队列中拿到当前线程独立的栈中
                // td->threadpool->unlockQueue();
            }
            std::cout << td->name << " 获取了一个任务" << t.toTaskString() << "并处理完成,结果是: " << t() << std::endl;
        }
        delete td;
        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)
        {
            _lock.lock();
            if (tp == nullptr)
            {
                tp = new ThreadPool<T>();
            }
            _lock.unlock();
        }
        return tp;
    }
private:
    int _num;
    std::vector<Thread *> _threads;
    std::queue<T> _task_queue;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
    static ThreadPool<T> *tp;
    static std::mutex _lock;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;
template <class T>
std::mutex ThreadPool<T>::_lock;

STL, smart pointers and thread safety

The STL container is not thread-safe: the original intention of STL is to maximize the performance, and once it involves locking to ensure thread safety, it will have a huge impact on performance. And for different containers, the locking method is different, Performance may also vary. Therefore, STL is not thread-safe by default. If it needs to be used in a multi-threaded environment, the caller often needs to ensure thread safety by itself.

For unique_ptr, since it only takes effect within the scope of the current code block, it does not involve thread safety issues. For shared_ptr, multiple objects need to share a reference count variable, so there will be thread safety issues. However, this issue is considered when the standard library is implemented , based on the atomic operation (CAS) method to ensure that shared_ptr can be efficient and atomic operation reference counting.

Other locks (understand)

悲观锁: Every time you fetch data, you always worry that the data will be modified by other threads, so you will lock (read lock, write lock, row lock, etc.) before fetching data. When other threads want to access data, they will be blocked hang up.

乐观锁: Every time data is fetched, it is always optimistic that the data will not be modified by other threads, so it is not locked. But before updating the data, it will judge whether other data have modified the data before updating. There are two main methods: version number mechanism and CAS operation.

CAS操作: When the data needs to be updated, judge whether the current memory value is equal to the previously acquired value. If equal update with new value. If it is not equal, it will fail, and if it fails, it will be retried. Generally, it is a spinning process, that is, it will continue to retry.

自旋锁: When using a spin lock, when multiple threads compete for a lock, the thread that fails to lock will wait busy (the busy waiting here can be realized by waiting in a while loop) until it gets the lock. After the mutex lock fails, the thread will give up CPU resources for other threads to use, and then the thread will be blocked and suspended. If the thread successfully applies for critical resources, the execution time of the critical section code is too long, and the spinning thread will occupy CPU resources for a long time, so the spin time is directly proportional to the execution time of the critical section code. If the execution time of the critical section code is very short, the mutex should not be used, but the spin lock should be used. Because the mutex lock fails, context switching needs to occur. If the execution time of the critical section is relatively short, the context switching time may be longer than the execution time of the critical section code.

Reader Writer Questions (Understanding)

A read-write lock consists of two parts: a read lock and a write lock. If you only read shared resources, use a read lock to lock them. If you want to modify shared resources, use a write lock to lock them.

image-20230426092116804

If the write lock is not held by the write thread, multiple read threads can hold the lock concurrently, improving the access efficiency of shared resources. Because read locks are used to read shared resources, multiple threads holding read locks will not destroy shared resources. Resource data.

Once the write lock is held by the thread, the operation of the read thread to acquire the lock will be blocked, and the operation of other writer threads to acquire the write lock will also be blocked

Note: write exclusive, read shared, read lock priority is high

The essential difference between the reader-writer problem and the producer-consumer model is that 消费者会取走数据,而读者不会取走数据.

Read-write lock interface

//初始化读写锁
pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);


//销毁读写锁
pthread_rwlock_destroy(pthread_rwlock_t *rwlock);


//读加锁
pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);


//写加锁
pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);


//解锁
pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

Guess you like

Origin blog.csdn.net/weixin_60478154/article/details/130379531