线程池的原理与实现

线程池的原理

线程池适用的场景

当遇到特别耗时的任务,影响其它线程的执行,需要在其他线程异步执行耗时的任务,进而平衡线程资源的开销与cpu核心,做到既能减少线程创建和销毁的开销,又能最大限度的提高 cpu 利用率。

线程池的作用

  • 复用线程资源
  • 充分利用系统资源
  • 异步执行耗时任务

线程池的构成

线程池是一个生产者消费者模型,由生产者线程和线程池(消费者)构成,通过任务队列进行异步解耦,提高程序运行效率,复用线程资源,减少线程创建和销毁的开销。
生产者线程作用是发布任务;任务队列用来存储任务结点,其中包括任务山下文,任务的执行函数等;线程池作用是取出任务,执行任务,在这个过程涉及到线程调度的问题,即从无到有唤醒,从有到无休眠。

线程结构

线程数量的平衡选择

线程数量的经验公式 (io等待时间+cpu运算时间)*核心数 / CPU运算时间

  • io密集型: 2*cpu核心数+有效词盘数
  • cpu密集型: cpu核心数

线程池运行原理

线程池作为一个组件,使用线程池的时候,我们需要调用哪些api实现业务。

  • 线程池初始化和创建:init 、create
  • 抛任务给线程池:push_task
  • 销毁:destroy

对于线程池内部实现逻辑如下:

  • thread_pool_create
  • thread_worker:取出任务,执行任务,任务调度,for遍历,通过mutex + condition
  • thread_pool_post:发布任务,对队列操作,改变状态后给线程发信号,此时使用自旋锁,任务发布状态完成,分发给线程
  • do_task:做具体的任务
  • wait_all_done:等待所有线程都安全退出
  • thread_pool_destroy:销毁线程池,具体为:组织产生新任务,通过pthread_cond_broadcast广播的形式,让阻塞在condition 上面的线程唤醒,等待他们退出

线程池的实现

  • 执行函数,上下文,任务队列,线程池定义:
// 任务 执行函数。 上下文
typedef struct task_t {
    handler_pt func;
    void * arg;
} task_t;

// 任务队列
typedef struct task_queue_t {
    uint32_t head;
    uint32_t tail;
    uint32_t count;
    task_t *queue;
} task_queue_t;

// 用到的锁 mutex + condition ,会创建一些线程,需要一个任务队列 
struct thread_pool_t {
    pthread_mutex_t mutex;
    pthread_cond_t condition;
    pthread_t *threads;
    task_queue_t task_queue;

    // 标记线程退出 
    int closed;
    int started; // 当前运行的线程数

    // 记录线程的数量  队列大小
    int thrd_count;
    int queue_size;
};
  • 创建线程池
    thrd_count:由经验公式计算得出,queue_size:由线程数量和线程栈空间大小共同决定
thread_pool_t *
thread_pool_create(int thrd_count, int queue_size) {
    thread_pool_t *pool;

    if (thrd_count <= 0 || queue_size <= 0) {
        return NULL;
    }

    pool = (thread_pool_t*) malloc(sizeof(*pool));
    if (pool == NULL) {
        return NULL;
    }

    pool->thrd_count = 0;
    pool->queue_size = queue_size;
    pool->task_queue.head = 0;
    pool->task_queue.tail = 0;
    pool->task_queue.count = 0;

    pool->started = pool->closed = 0;

    pool->task_queue.queue = (task_t*)malloc(sizeof(task_t)*queue_size);
    if (pool->task_queue.queue == NULL) {
        // TODO: free pool
        return NULL;
    }

    pool->threads = (pthread_t*) malloc(sizeof(pthread_t) * thrd_count);
    if (pool->threads == NULL) {
        // TODO: free pool
        return NULL;
    }

    int i = 0;
    for (; i < thrd_count; i++) {
        if (pthread_create(&(pool->threads[i]), NULL, thread_worker, (void*)pool) != 0) {
            // TODO: free pool
            return NULL;
        }
        pool->thrd_count++;
        pool->started++;
    }
    return pool;
}
  • 生产者抛出任务
//实现生产者 
int
thread_pool_post(thread_pool_t *pool, handler_pt func, void *arg) {
    if (pool == NULL || func == NULL) {
        return -1;
    }

    task_queue_t *task_queue = &(pool->task_queue);

    if (pthread_mutex_lock(&(pool->mutex)) != 0) {
        return -2;
    }

    if (pool->closed) {
        pthread_mutex_unlock(&(pool->mutex));
        return -3;
    }

    if (task_queue->count == pool->queue_size) {
        pthread_mutex_unlock(&(pool->mutex));
        return -4;
    }

    //操作队列 从队列尾部添加,头部取出,通过数组,一个头指针一个尾指针
    //这里用的互斥锁,用自旋锁效率高一点
    task_queue->queue[task_queue->tail].func = func;
    task_queue->queue[task_queue->tail].arg = arg;
    // tail可能大于数组大小
    task_queue->tail = (task_queue->tail + 1) % pool->queue_size;
    task_queue->count++;

    //用于通知线程条件变量已经满足条件,一定要在改变条件状态以后再给线程发信号
    if (pthread_cond_signal(&(pool->condition)) != 0) {
        pthread_mutex_unlock(&(pool->mutex));
        return -5;
    }
    pthread_mutex_unlock(&(pool->mutex));
    return 0;
}

  • 线程池消费任务
// 取出任务, 执行任务, 任务调度
static void *
thread_worker(void *thrd_pool) {
    thread_pool_t *pool = (thread_pool_t*)thrd_pool;
    task_queue_t *que;
    task_t task;
    for (;;) {
        pthread_mutex_lock(&(pool->mutex));
        que = &pool->task_queue;
        // while作用防止虚假唤醒   linux  pthread_cond_signal
        // linux 可能被信号唤醒
        // 业务逻辑不严谨,被其他线程抢了该任务
        while (que->count == 0 && pool->closed == 0) {
            // pthread_mutex_unlock(&(pool->mutex))
            // 当前线程就会阻塞在 condition
            // ===================================(阻塞)
            // 解除阻塞
            // pthread_mutex_lock(&(pool->mutex));
            pthread_cond_wait(&(pool->condition), &(pool->mutex));
        }
        if (pool->closed == 1) break;
        task = que->queue[que->head];
        que->head = (que->head + 1) % pool->queue_size;
        que->count--;
        pthread_mutex_unlock(&(pool->mutex));
        (*(task.func))(task.arg);
    }
    pool->started--;
    pthread_mutex_unlock(&(pool->mutex));
    pthread_exit(NULL);
    return NULL;
}
  • 销毁线程池
int
thread_pool_destroy(thread_pool_t *pool) {
    if (pool == NULL) {
        return -1;
    }

    //阻止产生新任务
    if (pthread_mutex_lock(&(pool->mutex)) != 0) {
        return -2;
    }

    if (pool->closed) {
        thread_pool_free(pool);
        return -3;
    }

    pool->closed = 1;

    //广播,让所有阻塞在condition上面的线程都唤醒,等待他们退出
    //pthread_cond_signal作用相同,这个是广播形式
    if (pthread_cond_broadcast(&(pool->condition)) != 0 || 
            pthread_mutex_unlock(&(pool->mutex)) != 0) {
        thread_pool_free(pool);
        return -4;
    }

    wait_all_done(pool);

    thread_pool_free(pool);
    return 0;
}

// 让线程都安全退出
int
wait_all_done(thread_pool_t *pool) {
    int i, ret=0;
    for (i=0; i < pool->thrd_count; i++) {
        if (pthread_join(pool->threads[i], NULL) != 0) {
            ret=1;
        }
    }
    return ret;
}

static void 
thread_pool_free(thread_pool_t *pool) {
    if (pool == NULL || pool->started > 0) {
        return;
    }

    if (pool->threads) {
        free(pool->threads);
        pool->threads = NULL;

        pthread_mutex_lock(&(pool->mutex));
        pthread_mutex_destroy(&pool->mutex);
        pthread_cond_destroy(&pool->condition);
    }

    if (pool->task_queue.queue) {
        free(pool->task_queue.queue);
        pool->task_queue.queue = NULL;
    }
    free(pool);
}
  • 测试代码
    设置线程数量为8,队列大小为256,测试不断抛任务给队列的同时进行任务处理,直到队列满了,线程池安全退出。结果表明能进行263个任务处理。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#include "thrd_pool.h"

//添加任务就会有nums++
int nums = 0;
//具体任务
int done = 0;

pthread_mutex_t lock;

void do_task(void *arg) {
    usleep(10000);
    pthread_mutex_lock(&lock);
    done++;
    printf("doing %d task\n", done);
    pthread_mutex_unlock(&lock);
}

int main(int argc, char **argv) {
    //默认值
    int threads = 8;
    int queue_size = 256;

    if (argc == 2) {
        threads = atoi(argv[1]);
        if (threads <= 0) {
            printf("threads number error: %d\n", threads);
            return 1;
        }
    } else if (argc > 2) {
        threads = atoi(argv[1]);
        queue_size = atoi(argv[1]);
        if (threads <= 0 || queue_size <= 0) {
            printf("threads number or queue size error: %d,%d\n", threads, queue_size);
            return 1;
        }
    }
    
    thread_pool_t *pool = thread_pool_create(threads, queue_size);
    if (pool == NULL) {
        printf("thread pool create error!\n");
        return 1;
    }
    
    //添加任务,
    while (thread_pool_post(pool, &do_task, NULL) == 0) {
        pthread_mutex_lock(&lock);
        nums++;
        pthread_mutex_unlock(&lock);
    }

    printf("add %d tasks\n", nums);
    
    wait_all_done(pool);

    printf("did %d tasks\n", done);
    thread_pool_destroy(pool);
    return 0;
}

  • 测试结果

1
2

线程池在具体场景的应用

redis为什么需要线程池

redis不是cpu密集型,因为内存的操作很快,而瓶颈来自于网络io,所以redis 选择了单线程的 I/O 多路复用来实现它的核心网络模型。
当客户端写大量的io请求(写日志)给服务器,redis的读压力很大,客户端向服务器读大量的io请求,redis的写压力就很大,此时就需要线程池进行处理,主线程首先收集所有读请求(read、decode)到一个队列(clients_pending_read)中,把每个事件负载均衡地分配给多个io线程中,主线程将解析后的任务做compute操作,写请求(encode、send)同理。
1213

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值