linux网络编程-实现线程池c++

1.为啥要用线程池

避免线程频繁的创建销毁导致资源开销较大,可以提前创建一定数量的线程以供需要的时候随去随用。

2.线程池可以用在哪些地方

现代服务器都使用epoll、select这样的I/O多路复用技术——它们让CPU在等待网络数据时不会闲着,可以服务其他客户端,从而大幅提升并发性能

存在并发任务处理且需要控制线程资源的场景:

  • 网络服务器(如 Web 服务器、Socket 服务器):需要处理大量客户端的并发请求(如 HTTP 请求、TCP 连接),线程池可快速分配线程处理请求,避免为每个请求单独创建线程导致的资源耗尽。

  • 数据库服务器:处理多用户的数据库查询、事务操作等并发任务,通过线程池控制数据库连接和任务处理的并发度。

3.如何构建一个线程池

1.首先需要构建三个结构体(线程池关键的三个部分)

任务结构体:一个先进先出的队列

struct nTask {
	void (*task_func)(struct nTask *task);  // 存储“要执行的函数”和“函数的参数”
	void *user_data;                        // 用户数据

	struct nTask *prev;                     // 前驱指针,用于链表操作
	struct nTask *next;                     // 后继指针,用于链表操作
};

工作者线程结构体:用于从任务队列取任务并执行

struct nWorker {
	pthread_t threadid;      // 线程ID
	int terminate;           // 终止标志
	struct nManager *manager; // 指向线程池管理器

	struct nWorker *prev;    // 前驱指针,用于链表操作
	struct nWorker *next;    // 后继指针,用于链表操作
};

线程池管理器结构体:线程池的管理者,它用于给任务队列添加任务交给工作者线程执行

互斥锁:切换线程,锁的内容较多

自旋锁:等待时间小于切换的时间,锁的内容较少

原子操作:把三条指令变为一条指令,单条CPU指令xaddl

CAS :compare and swap

typedef struct nManager {
	struct nTask *tasks;     // 任务链表头指针
	struct nWorker *workers; // 工作者线程链表头指针

	pthread_mutex_t mutex;   // 互斥锁,保护共享数据
	pthread_cond_t cond;     // 条件变量,用于线程同步
} ThreadPool;

2.宏定义链表的插入和删除

#define LIST_INSERT(item, list) do {	\
	item->prev = NULL;					\
	item->next = list;					\
	if ((list) != NULL) (list)->prev = item; \
	(list) = item;						\
} while(0)


#define LIST_REMOVE(item, list) do {	\
	if (item->prev != NULL) item->prev->next = item->next; \
	if (item->next != NULL) item->next->prev = item->prev; \
	if (list == item) list = item->next; 					\
	item->prev = item->next = NULL;							\
} while(0)

3.线程池的创建初始化

pthread_create 的第四个参数类型是 void*(无类型指针)—— 这是 C 语言的 “通用参数” 设计:不管你要传什么类型的数据(int、结构体、指针等),都能转换成 void* 传进去,让 pthread_create 能适配各种场景。

int nThreadPoolCreate(ThreadPool *pool, int numWorkers) {
	if (pool == NULL) return -1;  // 参数检查
	if (numWorkers < 1) numWorkers = 1;  // 至少创建一个工作者线程
	
	// 初始化线程池结构体
	memset(pool, 0, sizeof(ThreadPool));

	// 初始化条件变量(使用静态初始化方式)
	pthread_cond_t blank_cond = PTHREAD_COND_INITIALIZER;
	memcpy(&pool->cond, &blank_cond, sizeof(pthread_cond_t));

	// 初始化互斥锁(使用静态初始化方式)
	pthread_mutex_t blank_mutex = PTHREAD_MUTEX_INITIALIZER;
	memcpy(&pool->mutex, &blank_mutex, sizeof(pthread_mutex_t));
	
	// 创建指定数量的工作者线程
	int i = 0;
	for (i = 0; i < numWorkers; i++) {
		// 分配工作者线程内存
		struct nWorker *worker = (struct nWorker*)malloc(sizeof(struct nWorker));
		if (worker == NULL) {
			perror("malloc");
			return -2;  // 内存分配失败
		}   
		memset(worker, 0, sizeof(struct nWorker));
		worker->manager = pool;  // 设置线程池管理器指针

		// 创建工作者线程
		int ret = pthread_create(&worker->threadid, NULL, nThreadPoolCallback, worker);
		if (ret) {
			perror("pthread_create");
			free(worker);
			return -3;  // 线程创建失败
		}
		
		// 将工作者线程插入链表
		LIST_INSERT(worker, pool->workers);
	}

	return 0;  // 创建成功
}

4.回调函数

异步执行,不影响主程序的操作

线程创建好不一定有任务,所以线程需要提前判断是否有任务存在,如果没有任务,那么继续阻塞状态继续等待,while循环暂停。若有任务则先会被唤醒,while循环继续,然后接受到任务去执行它。直到接受到终止信号退出,释放内存。

struct nWorker *worker = (struct nWorker*)arg;

struct nWorker* 类型的指针(指向你用 malloc 申请的 “工作者线程内存”,里面存着线程 ID、所属线程池、运行状态等信息)。

传递时的逻辑struct nWorker* worker → 转换成 void* 类型 → 作为 arg 传给 nThreadPoolCallback 函数。

static void *nThreadPoolCallback(void *arg) {
	struct nWorker *worker = (struct nWorker*)arg;  // 获取工作者线程信息

	while (1) {
		// 加锁访问共享数据
		pthread_mutex_lock(&worker->manager->mutex);
		
		// 等待条件:有任务可执行或收到终止信号
		while (worker->manager->tasks == NULL) {
			if (worker->terminate) break;  // 如果收到终止信号,退出等待
			pthread_cond_wait(&worker->manager->cond, &worker->manager->mutex);  // 等待条件变量
		}
		
		// 检查是否终止
		if (worker->terminate) {
			pthread_mutex_unlock(&worker->manager->mutex);
			break;  // 退出线程循环
		}

		// 从任务链表中获取任务
		struct nTask *task = worker->manager->tasks;
		LIST_REMOVE(task, worker->manager->tasks);  // 从链表中移除任务

		// 解锁,允许其他线程访问共享数据
		pthread_mutex_unlock(&worker->manager->mutex);

		// 执行任务函数  
		task->task_func(task);
   
	}

	// 释放工作者线程内存
	free(worker);
	return NULL;
}

5.向线程池添加任务

有任务来了,就需要唤醒一个线程去执行

int nThreadPoolPushTask(ThreadPool *pool, struct nTask *task) {
	// 加锁保护共享数据
	pthread_mutex_lock(&pool->mutex);

	// 将任务插入任务链表
	LIST_INSERT(task, pool->tasks);

	// 通知一个等待的工作者线程有任务可执行
	pthread_cond_signal(&pool->cond);

	// 解锁
	pthread_mutex_unlock(&pool->mutex);

	return 0; 
}

6.销毁线程池

先把每个线程终止掉,然后广播让它们释放掉内存,最后清空任务队列和工作者队列。

int nThreadPoolDestory(ThreadPool *pool, int nWorker) {
	struct nWorker *worker = NULL;

	// 设置所有工作者线程的终止标志
	for (worker = pool->workers; worker != NULL; worker = worker->next) {
		worker->terminate = 1;  //
	}

	// 加锁保护共享数据
	pthread_mutex_lock(&pool->mutex);

	// 广播条件变量,唤醒所有等待的工作者线程
	pthread_cond_broadcast(&pool->cond);

	// 解锁
	pthread_mutex_unlock(&pool->mutex);

	// 清空链表指针
	pool->workers = NULL;
	pool->tasks = NULL;

	return 0;
}

7.任务执行函数

void task_entry(struct nTask *task) {
	// 从用户数据中获取任务索引
	int idx = *(int *)task->user_data;
	printf("idx: %d\n", idx);

	// 释放任务相关的内存
	free(task->user_data);
	free(task);
}

8.主函数

#define THREADPOOL_INIT_COUNT	20    // 初始线程数量
#define TASK_INIT_SIZE			1000  // 初始任务数量
int main(void) {
	ThreadPool pool = {0};  // 初始化线程池结构体
	
	// 创建线程池
	nThreadPoolCreate(&pool, THREADPOOL_INIT_COUNT);
	
	// 创建并提交多个任务
	int i = 0;
	for (i = 0; i < TASK_INIT_SIZE; i++) {
		// 分配任务内存
		struct nTask *task = (struct nTask *)malloc(sizeof(struct nTask));
		if (task == NULL) {
			perror("malloc");
			exit(1);
		}
		memset(task, 0, sizeof(struct nTask));

		// 设置任务函数和用户数据
		task->task_func = task_entry;
        // 给任务分配一块堆内存,把任务索引 i 存进去
        //让任务执行时能拿到这个唯一索引;
        //用堆内存是为了确保参数在任务执行前不被回收,保证数据正确
		task->user_data = malloc(sizeof(int));,
		*(int*)task->user_data = i;  
		// 将任务提交到线程池
		nThreadPoolPushTask(&pool, task);
        }
	    // 等待用户输入(保持程序运行)
	    getchar();
	    // 销毁函数来清理资源
	    nThreadPoolDestory(&pool, THREADPOOL_INIT_COUNT);
	    
	return 0;
}
	}

https://github.com/0voice

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值