C语言写一个线程池(原理 + 代码)

C语言写一个线程池

这个是我很久之前写的了,今天发出来做一下纪念,顺便回顾一下线程池的知识。

线程池的原理

想象地面上有一群鸽子(类比于进程),我们手里拿着面包屑(类比于任务)喂他们,当我们扔出一个面包屑的时候(类比于出现了一个任务),鸽子们蜂拥而上,抢夺那个面包屑,最终只有一个鸽子能吃到面包屑,而其他的鸽子则继续等待新的面包屑的到来。

上面的这个过程就是线程池的原理。

我们创建多个线程,让他们进入等待状态,然后再创建一个任务队列,当任务队列中存在任务时,线程们会去取得任务的执行权限(大部分情况下是去抢夺一块内存空间),之后通过互斥锁告诉其他线程该任务已经被抢到了,其他线程则继续抢夺队列中的其他任务,直到为空为止。

线程池示例

接下来,我们以输出一个文件中每一行的内容作为任务为例子,写一个线程池的代码。

首先,明确我们的任务:
获取文件中第一行未获取的字符串,并打印出来。

那该如何设计呢?首先是需要获取任务,我们可以让一个进程去获取文件中每一行的字符的空间地址,作为一个任务放进队列中,之后让线程们去输出。这是我们的基本思路。

任务队列的设计

接下来我们就需要设计任务队列了,根据我们的需求,任务队列可以这样设计:

struct task_queue{
    int size; // 队列大小
    int total; // 队列当前任务个数
    int head; // 头指针
    int tail; // 尾指针
    char **data; // 数据地址
    pthread_mutex_t mutex; // 锁
    pthread_cond_t cond; // 信号量
};

相关函数设计

我们对于相关函数的设计都是基于任务队列实现的,具体如下:

  • 任务队列的初始化
  • 在任务队列中添加任务
  • 弹出任务
  • 运行任务

因此有下面四个函数:

void task_queue_init(struct task_queue *taskQueue, int size);
void task_queue_push(struct task_queue *taksQueue, char *str);
char *task_queue_pop(struct task_queue *taskQueue);
void *thread_run(void *arg);

以上就是头文件中的内容:

#ifndef _THREAD_POOL_H
#define _THREAD_POOL_H

struct task_queue{
    int size;
    int total;
    int head;
    int tail;
    char **data;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
};

void task_queue_init(struct task_queue *taskQueue, int size);
void task_queue_push(struct task_queue *taksQueue, char *str);
char *task_queue_pop(struct task_queue *taskQueue);
void *thread_run(void *arg);

#endif

函数实现

任务队列初始化

这个函数的功能主要是为了确定队列的大小以及头尾指针的位置,确保后面的其他相关函数能在队列上正确运行。

void task_queue_init(struct task_queue *taskQueue, int size) {
    taskQueue->size = size;
    taskQueue->total = taskQueue->head = taskQueue->tail = 0;
    pthread_mutex_init(&taskQueue->mutex, NULL);
    pthread_cond_init(&taskQueue->cond, NULL);
    taskQueue->data = calloc(size, sizeof(void *));
    return ;
}

添加任务

回想一下刚才设计的任务内容,每个线程是用于输出一串字符串的,字符串从文件中获取。因此我们在任务队列中需要传入每个字符串的地址。

需要注意的是,在添加任务时,也需要注意空间访问的竟态性,也就是说我们需要加上锁。

同时,仅仅是添加任务不行,还需要让线程们知道队列中有任务。因此需要使用条件变量告诉线程们这里有任务。

使用轮询替代条件变量可以吗?不可以,因为线程池存在的意义是用来提高程序效率的,如果使用轮询,那么会出现很多线程在CPU上“空转”的情况,这样会大大降低效率,吃力不讨好。

void task_queue_push(struct task_queue *taskQueue, char *str) {
    // 加锁
    pthread_mutex_lock(&taskQueue->mutex);
    // 任务队列满
    if (taskQueue->total == taskQueue->size) {
    	// 解锁并且返回
    	printf(RED"taskQueue is full!\n"NONE);
        pthread_mutex_unlock(&taskQueue->mutex);
        return ;
    }
    // 在队列尾部添加字符串地址
    taskQueue->data[taskQueue->tail] = str;
    taskQueue->total++; // 队列任务数量加一
	// 注意需要维护循环队列的性质
    if (++taskQueue->tail == taskQueue->size) {
        taskQueue->tail = 0;
    }
    printf(GREEN"<D> data is pushed to taskQueue!\n"NONE);
    
    pthread_cond_signal(&taskQueue->cond); // 告诉线程们存在任务
    pthread_mutex_unlock(&taskQueue->mutex); // 释放空间
}

弹出任务

这个函数用于从任务队列的头部弹出任务,也就是起到分发任务的作用。

同样要注意,任务的获取也是多线程中对任务空间的竟态访问,因此需要加锁

还有一点,这里与鸽子吃面包不同的是,线程们并不是去争抢一个任务,而是排队进行,具体原因是体现在pthread_cond_wait这个函数上。

简单来说,在上一个函数的代码中,倒数第二行代码发送了一个信号:

pthread_cond_signal(&taskQueue->cond); // 告诉线程们存在任务

但实际上,这个信号只会告诉一个线程让其摆脱阻塞状态,也就是说,只有一个线程会通过pthread_cond_wait函数收到信号。然后去执行这个任务。

char *task_queue_pop(struct task_queue *taskQueue) {
    pthread_mutex_lock(&taskQueue->mutex);
    // 核心语句需要好好理解
    while (taskQueue->total == 0) {
    	// 释放锁并等待信号
        pthread_cond_wait(&taskQueue->cond, &taskQueue->mutex);
    }
    // 维护队列的性质
    // 获取字符串
    char *str = taskQueue->data[taskQueue->head];
    taskQueue->total--;
    printf("<D> taskQueue pop: %s\n", str);
    if (++taskQueue->head == taskQueue->size) {
        printf(RED"<D> taskQueue touched tail\n"NONE);
        taskQueue->head = 0;
    }
    pthread_mutex_unlock(&taskQueue->mutex);
    return str;
}

任务的运行

这个函数规定了每个线程需要做的任务,简单来说就是从任务队列中取出任务并且输出。

在这里需要注意的是,在线程池中,线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而直接自己自动释放(自己清理掉PCB的残留资源),因此我们需要执行线程分离,也就是pthread_detach()函数。

void *thread_run(void *arg) {
	// 首先执行线程分离
    pthread_detach(pthread_self());
    // 得到任务队列的地址
    struct task_queue *taskQueue = (struct task_queue *)arg;
    while (1) {
    	// 从任务队列中取出任务
        char *str = task_queue_pop(taskQueue);
        printf("%s\n", str);
    }
}

总结一下代码:

thread_pool.h:

#ifndef _THREAD_POOL_H
#define _THREAD_POOL_H

struct task_queue{
    int size;
    int total;
    int head;
    int tail;
    char **data;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
};

void task_queue_init(struct task_queue *taskQueue, int size);
void task_queue_push(struct task_queue *taksQueue, char *str);
char *task_queue_pop(struct task_queue *taskQueue);
void *thread_run(void *arg);

#endif

thread.pool.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include "./thread_pool.h"

void task_queue_init(struct task_queue *taskQueue, int size) {
    taskQueue->size = size;
    taskQueue->total = taskQueue->head = taskQueue->tail = 0;
    pthread_mutex_init(&taskQueue->mutex, NULL);
    pthread_cond_init(&taskQueue->cond, NULL);
    taskQueue->data = calloc(size, sizeof(void *));
    return ;
}

void task_queue_push(struct task_queue *taskQueue, char *str) {
    pthread_mutex_lock(&taskQueue->mutex);
    if (taskQueue->total == taskQueue->size) {
        DBG(RED"<D> taskQueue is full!\n"NONE);
        pthread_mutex_unlock(&taskQueue->mutex);
        return ;
    }
    taskQueue->data[taskQueue->tail] = str;
    taskQueue->total++;
    if (++taskQueue->tail == taskQueue->size) {
        taskQueue->tail = 0;
    }
    DBG(GREEN"<D> data is pushed to taskQueue!\n"NONE);
    pthread_cond_signal(&taskQueue->cond);
    pthread_mutex_unlock(&taskQueue->mutex);
}

char *task_queue_pop(struct task_queue *taskQueue) {
    pthread_mutex_lock(&taskQueue->mutex);
    while (taskQueue->total == 0) {
        pthread_cond_wait(&taskQueue->cond, &taskQueue->mutex);
    }
    char *str = taskQueue->data[taskQueue->head];
    taskQueue->total--;
    DBG(GREEN"<D> taskQueue pop: %s\n"NONE, str);
    if (++taskQueue->head == taskQueue->size) {
        DBG(RED"<D> taskQueue touched tail\n"NONE);
        taskQueue->head = 0;
    }
    pthread_mutex_unlock(&taskQueue->mutex);
    return str;
}

void *thread_run(void *arg) {
    pthread_detach(pthread_self());
    struct task_queue *taskQueue = (struct task_queue *)arg;
    while (1) {
        char *str = task_queue_pop(taskQueue);
        printf("%s\n", str);
    }
}

main.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include "./thread_pool.h"

#define THREAD_NUM 10i // 线程数量
#define QUEUE_SIZE 100 // 队列长度
#define MAX_BUFF_LENGTH 1024 // 缓冲区长度

int main(){
	// 线程id
    pthread_t tid[THREAD_NUM];
    struct task_queue taskQueue;
    // buff用于记录文件中每一行的内容
    char buff[QUEUE_SIZE + 2][MAX_BUFF_LENGTH] = {0};
    task_queue_init(&taskQueue, QUEUE_SIZE);
    // 创建线程并且让他们跑起来
    for (int i = 0; i < THREAD_NUM; i++) {
        pthread_create(&tid[i], NULL, thread_run, (void *)&taskQueue);
    }

	// 分发任务
    int sub = 0; // sub用于记录buff的下标 
    while(1) {
    	// 这里以读取线程池的文件为例子
        FILE *fp = fopen("./thread_pool.c", "r");
        if (fp == NULL) {
            perror("fopen");
            exit(1);
        }
        // 每次取得一行
        while(fgets(buff[sub], MAX_BUFF_LENGTH, fp) != NULL) {
            // 放入任务队列
            task_queue_push(&taskQueue, buff[sub]);
            if (++sub >= QUEUE_SIZE) sub = 0;
        }
        // 任务队列满了,需要过一段时间再放入,这个时候让主线程睡一会
        if (taskQueue.total == taskQueue.size) {
            while (1) {
                usleep(1000000);
                if (taskQueue.total < taskQueue.size) break;
            }
            sleep(1);
        }
        fclose(fp);
    }
    return 0;
}
  • 16
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

若亦_Royi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值