C++手写实现线程池(MacOS和Linux系统)

引言

高并发程序开发中我们经常使用到线程池, 无论Java语言还是C++, 我们就拿Java语言的线程池举例, 面试八股文中通常都背诵的滚瓜烂熟, 比如, 核心线程池数量(core size), 最大线程池数量, 超时时间, 队列长度, 以及拒绝策略等等面试官可能都会问到, 但是今天广联达的面试官问到我线程池底层是怎么实现的? 来一个任务它是直接被调度起来, 还是放到队列中去的? 这点之前我确实没仔细琢磨过(没看过Java线程池的底层的实现原理), 今天带着这个问题, 我阅读了一下Java线程池的底层实现, 并且使用C++手写了一个简单的线程池, 这里记录一下实现的过程及一些细节问题.

实现过程

1. 定义Runnable接口

这点相信不用多说, Java线程中我们都会实现Runnable接口, 然后线程调用start()方法之后会调用我们传递进去的runnable接口的run()方法, C++这里我也做了一个实现:

#ifndef RUNNABLE_H
#define RUNNABLE_H
#include 
class runnable {
public:
    virtual void run() = 0;
    ~runnable() {
        std::cout << "destroy runnable object" << std::endl;
    }
};
#endif

当然也可以使用模板类定义一些有参的run()方法, 这些都无关紧要.

2. 实现信号量控制和锁

信号量类: 目的是为了方式队列中任务数量为空, 导致while(true)代码块出现空转, 占用CPU资源;
锁类: 目的是保证多个线程从队列中取任务和添加任务到队列中访问是互斥的;

#ifndef _LOCKER_H_
#define _LOCKER_H_
#include 
#ifdef __APPLE__
#include 
#else
#include 
#endif
#include 

class locker {
private:
    pthread_mutex_t mutex;

public:
    locker() {
        if (pthread_mutex_init(&(this->mutex), nullptr) != 0) {
            throw std::exception();
        }
    }
    ~locker() {
        pthread_mutex_destroy(&(this->mutex));
    }
    bool lock() {
        return pthread_mutex_lock(&(this->mutex)) == 0;
    }
    bool unlock() {
        return pthread_mutex_unlock(&(this->mutex)) == 0;
    }
};

class sem {
public:
    sem() {
#ifdef __APPLE__
        m_sem = dispatch_semaphore_create(0);
#else
        if (sem_init(&m_sem, 0, 0) != 0) {
            throw std::exception();
        }
#endif
    }
    sem(int init_num) {
#ifdef __APPLE__
        m_sem = dispatch_semaphore_create(init_num);
#else
        if (sem_init(&(this->m_sem), 0, init_num) != 0) {
            throw std::exception();
        }
#endif
    }
    ~sem() {
#ifdef __APPLE__

#else
        sem_destroy(&m_sem);
#endif
    }

    bool wait() {
#ifdef __APPLE__
        return dispatch_semaphore_wait(m_sem, DISPATCH_TIME_FOREVER) == 0;
#else
        return sem_wait(&(this->m_sem)) == 0;
#endif
    }

    bool post() {
#ifdef __APPLE__
        return dispatch_semaphore_signal(m_sem) == 0;
#else
        return sem_post(&(this->m_sem)) == 0;
#endif
    }

private:
#ifdef __APPLE__
    dispatch_semaphore_t m_sem;
#else
    sem_t m_sem;
#endif
};
#endif

踩坑点: MAC OS系统没有对sem_post(), sem_wait(), sem_init(), sem_destroy()这几个方法进行实现, 因此在class sem类中定义了一些宏, 用于替代这几个方法的实现.

3. 实现线程池

实现逻辑简介:

  1. new一个线程池的时候, 会传递进来线程池大小m以及队列大小n, 并同时创建启动m个线程, 这m个县城都会竞争的从任务队列task_queue中抢任务执行;
  2. 初始的信号量semaphore = 0, 因此所有的线程都阻塞着在;
  3. 如果有任务添加到队列中, signal(semaphore)会唤醒相应数量的线程从队列中取数据执行;

实现代码:

#ifndef _THREADPOOL_H
#define _THREADPOOL_H
#include 
#include 
#include 
#include "locker.h"
#include "runnable.h"

class threadpool {
public:
    threadpool(int thread_number, int max_request);
    ~threadpool();
    bool append(std::shared_ptr<runnable> task);

private:
    int thread_number;                             // 线程数量
    pthread_t *threads;                            // 线程数量数组, 大小等于 thread_number
    int max_requests;                              // 队列中最大允许的任务数量
    std::queue<std::shared_ptr<runnable>> m_queue; // 任务队列
    sem m_sem;                                     // 任务队列存取的信号量, 值表示当前队列中剩余等待调度的任务数量
    locker m_queue_locker;                         // 队列写锁
    static void *worker(void *arg);                // c++线程执行的具体方法
    void run();
};

#endif

/**
 * @brief Construct a new threadpool::threadpool object
 * 有参 初始化线程池
 * @param thread_number 初始线程数量
 * @param max_request 最大队列数量
 */
threadpool::threadpool(int thread_number, int max_request) {
    if (thread_number <= 0 || max_request <= 0) {
        throw std::exception();
    }
    this->max_requests = max_request;
    this->threads = new pthread_t[thread_number];

    if (!this->threads) {
        throw std::exception();
    }
    for (int i = 0; i < thread_number; i++) {
        if (pthread_create(this->threads + i, nullptr, worker, this) != 0) {
            // create ans start thread failed
            delete[] this->threads;
            throw std::exception();
        }
        if (pthread_detach(this->threads[i]) != 0) {
            delete[] this->threads;
            throw std::exception();
        }
    }
}

threadpool::~threadpool() {
    delete[] this->threads;
}

/**
 * @brief 线程池执行函数体
 *
 * @param arg
 * @return void*
 */
void *threadpool::worker(void *arg) {
    threadpool *pool = (threadpool *)arg;
    pool->run();
    return pool;
}

/**
 * @brief 添加一个任务到线程池的队列中
 *
 * @param task 任务指针
 * @return true
 * @return false
 */
bool threadpool::append(std::shared_ptr<runnable> task) {
    this->m_queue_locker.lock(); // 阻塞获取锁
    if (this->m_queue.size() >= this->max_requests) {
        this->m_queue_locker.unlock();
        return false;
    }
    this->m_queue.push(task);
    this->m_queue_locker.unlock();
    this->m_sem.post();
    return true;
}

void threadpool::run() {
    while (true) {
        this->m_sem.wait();
        this->m_queue_locker.lock();
        if (this->m_queue.empty()) {
            this->m_queue_locker.unlock();
            continue;
        }
        auto task = this->m_queue.front();
        this->m_queue.pop();
        this->m_queue_locker.unlock();
        task->run();
    }
}

4. 测试线程池

测试代码:

#include 
#include "threadpool.h"

class my_runnable : public runnable {
public:
    my_runnable() {
    }

    void run() override {
        std::cout << "hello runnable" << std::endl;
        sleep(3);
    }
};

int main() {
    threadpool *pool = new threadpool(3, 10);
    for (int i = 0; i < 4; i++) {
        std::shared_ptr<runnable> task(new my_runnable());
        pool->append(task);
    }
    sleep(20);
    delete pool;
    return 0;
}

测试结果:
C++手写实现线程池(MacOS和Linux系统)_第1张图片常见问题:

  1. 队列模板使用了shared_ptr, 防止内存泄露;
  2. 拒绝策略没有实现, 后序可以在append()方法中添加相应的实现, 例如: 超过max_request值可直接throw std::exception(), 或者直接调用runnable接口中的run()方法, 这样就把main线程给阻塞掉了, 这也是Java线程池中常用的拒绝策略.

其它的未考虑到的点欢迎大家批评指正
Github代码链接

你可能感兴趣的:(Java相关,c++,macos,linux,java)