11.5 Linux_线程_线程池

概述

什么是线程池:

线程池就是线程的集合,里面存放了一系列的线程。

线程池使用于需要大量创建和销毁线程的情况,使用线程池比单个线程的创建和销毁速度要快。

线程池的结构:

任务队列:任务队列是任务的生产者,存储需要处理的任务,工作线程会处理这些任务。

线程池:线程池中存放着工作线程,它是任务队列任务的消费者,等待新任务的信号。 

固定大小线程池的实现:

1、创建线程池的基本结构:任务队列结构体、线程池结构体

2、线程池初始化:创建线程池结构、互斥锁和条件变量初始化、创建n个工作线程

3、线程池添加任务:判断是否有空闲线程、给任务队列添加节点、给工作线程发送信号

4、实现工作线程:等待任务信号、从任务队列中取出任务、执行任务

5、线程池销毁:删除任务队列、互斥锁、条件变量、线程池

线程池的实现

1、结构体实现

1.1 任务队列结构体

任务被抽象为一个函数,队列需要一个指针指向下一个节点。

具体结构体声明如下:

//任务队列
typedef struct Task{
	void* (*func)(void* arg);//任务处理函数
	void* arg;               //任务处理函数的参数
	struct Task* pNext;      //链表指针
}Task;

1.2 线程池结构体

任务是一个临界资源,同一时刻只允许有一个线程处理该任务,所以需要一个互斥量。

任务和工作线程为生产者和消费者的关系,所以需要一个条件变量。

线程池中有多个工作线程,所以需要一个线程TID数组

线程与任务之间需要交互,所以需要当前正在工作线程的个数、当前需要处理的任务

具体结构体声明如下:

//线程池
#define POOL_NUM 5 
typedef struct ThreadPool{
	pthread_mutex_t taskLock;    //互斥锁
	pthread_cond_t newTask;      //条件变量

	pthread_t tid[POOL_NUM];     //线程TID
	Task* queue_head;            //任务队列头部
	Task* queue_tail;    		 //任务队列尾部
	int busyWork;                //当前正在工作的线程数量
}ThreadPool;
ThreadPool* pPool;

2、创建和销毁线程池

2.1 初始化线程池

线程池初始化就是申请线程池空间,并初始化互斥量、条件变量、任务队列、工作线程数量、创建工作线程。

具体代码实现如下:

/*
 * pool_init:线程池初始化
 * @ret -1--err 0--success
 */
int pool_init(void){

	//1.申请空间
	if((pPool = malloc(sizeof(ThreadPool))) == NULL){
		printf("malloc err\n");
		return -1;
	}
	//2.初始化
	pthread_mutex_init(&(pPool->taskLock),NULL); 			//互斥量初始化
	pthread_cond_init(&(pPool->newTask),NULL); 				//条件变量初始化
	pPool->busyWork = 0;									//当前工作的线程=0
	if((pPool->queue_head = malloc(sizeof(Task))) == NULL){ //任务队列初始化
		printf("malloc err\n");
		return -1;
	}
	pPool->queue_tail = pPool->queue_head;
	pPool->queue_head->pNext = NULL;
	for(int i=0;i<POOL_NUM;i++){ 							//创建工作线程
		if(pthread_create(&(pPool->tid[i]),NULL,workThread,NULL) != 0){
			perror("pthread_create");
			return -1;//这里没有做清理工作
		}
	}
	return 0;
}

2.2 销毁线程池 

销毁线程池就是释放创建时申请的空间。销毁顺序为:任务队列、互斥量和条件变量、线程池

具体代码实现如下:

/*
 * pool_destroy:销毁线程池
 * */
void pool_destroy(void){
	Task* pTask = NULL;
	//1.销毁任务队列
	while(pPool->queue_head!=NULL){
		pTask = pPool->queue_head->pNext;
		free(pPool->queue_head);
		pPool->queue_head = pTask;
	}
	//2.销毁互斥锁、条件变量
	pthread_mutex_destroy(&(pPool->taskLock));
	pthread_cond_destroy(&(pPool->newTask));
	//3.销毁线程池
	free(pPool);
}

3、向线程池中添加任务(生产者)

该函数首先是生产者模型:加锁互斥量->生成资源->发送信号->解锁互斥量

除此之外,还需在生产资源之前判断是否存在空闲的线程,如果不存在则需等待一段时间再生产资源,这一步的目的是为了防止生产资源后发送的信号没有被工作线程接收,导致资源的丢失。

具体代码实现如下:

/*
 * pool_add_task:向线程池中添加任务
 * param pfun:任务处理函数
 * param arg:任务处理函数的参数
 * */
void pool_add_task(void*(pfun)(void*),void* arg){
	Task* pTask = NULL;
	//生产者代码框架
	pthread_mutex_lock(&(pPool->taskLock)); 		//上锁互斥量
	//判断当前是否有空闲的工作线程,如果不判断会导致条件变量信号丢失问题
	while(pPool->busyWork > POOL_NUM){
		pthread_mutex_unlock(&(pPool->taskLock));
		usleep(1000);
		pthread_mutex_lock(&(pPool->taskLock));
	}
	if((pTask = malloc(sizeof(Task))) == NULL){ 	//生产资源
		printf("malloc err\n");
		return;
	}
	pTask->func = pfun;
	pTask->arg = arg;
	pTask->pNext = NULL;
	pPool->queue_tail->pNext = pTask;
	pPool->queue_tail = pTask;
	pPool->busyWork++; 								//资源生产结束,代表将有一个线程会工作
	pthread_cond_signal(&(pPool->newTask)); 		//发送信号
	pthread_mutex_unlock(&(pPool->taskLock)); 		//解锁互斥量
}

4、从线程池中处理任务(消费者)

该函数首先是消费者模型:上锁互斥量->等待信号->获取资源->解锁互斥量

除此之外,这里的获取资源指的是将任务结构体获取到,而不是直接处理完成任务。任务的处理与生产者与消费者模型无关,所以可以放在解锁互斥量后面,这一步的目的是为了尽快解锁互斥量,让其他线程可以尽快处理新来的任务。

具体代码实现如下:

/*
 * workThread:工作线程
 * */
void* workThread(void* arg){
	Task* pTask = NULL;
	while(1){
		//消费者代码框架
		pthread_mutex_lock(&(pPool->taskLock)); 	//上锁互斥量
		while(pPool->queue_head->pNext == NULL){    //等待资源
			pthread_cond_wait(&(pPool->newTask),&(pPool->taskLock));
		}
		pTask = pPool->queue_head->pNext; 			//使用资源
		pPool->queue_head->pNext = pTask->pNext;
		if(pPool->queue_head->pNext == NULL){
			pPool->queue_tail = pPool->queue_head;
		}
		pthread_mutex_unlock(&(pPool->taskLock));   //解锁互斥量
		//资源中的任务与消费者无关,因此可以放在互斥量外
		pTask->func(pTask->arg); 					//执行相应任务函数
		free(pTask);
		pPool->busyWork--; 							//当前线程工作完成
	}
}

线程池完整代码

代码功能:

在main函数中不断的生产资源,生产资源后,工作线程会自动的处理资源。

具体代码实现如下:

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

//任务队列
typedef struct Task{
	void* (*func)(void* arg);//任务处理函数
	void* arg;               //任务处理函数的参数
	struct Task* pNext;      //链表指针
}Task;
//线程池
#define POOL_NUM 5 
typedef struct ThreadPool{
	pthread_mutex_t taskLock;    //互斥锁
	pthread_cond_t newTask;      //条件变量

	pthread_t tid[POOL_NUM];     //线程TID
	Task* queue_head;            //任务队列头部
	Task* queue_tail;    		 //任务队列尾部
	int busyWork;                //当前正在工作的线程数量
}ThreadPool;
ThreadPool* pPool;

int pool_init(void);
void* workThread(void* arg);
void pool_add_task(void* (pfun)(void*),void* arg);
void* task1(void* arg);
void pool_destroy(void);

int main(){
	
	int tmp = 1;
	pool_init();
	while(1){
		pool_add_task(task1,(void*)tmp);//注意:这里tmp是常变化的值,不能传入地址&tmp
		tmp++;
	}

	return 0;
}
/*
 * pool_destroy:销毁线程池
 * */
void pool_destroy(void){
	Task* pTask = NULL;
	//1.销毁任务队列
	while(pPool->queue_head!=NULL){
		pTask = pPool->queue_head->pNext;
		free(pPool->queue_head);
		pPool->queue_head = pTask;
	}
	//2.销毁互斥锁、条件变量
	pthread_mutex_destroy(&(pPool->taskLock));
	pthread_cond_destroy(&(pPool->newTask));
	//3.销毁线程池
	free(pPool);
}

/*
 * task1:模拟任务
 * */
void* task1(void* arg){
	printf("%ld\n",((long int)arg));
	sleep(1);
	return NULL;
}

/*
 * pool_add_task:向线程池中添加任务
 * param pfun:任务处理函数
 * param arg:任务处理函数的参数
 * */
void pool_add_task(void*(pfun)(void*),void* arg){
	Task* pTask = NULL;
	//生产者代码框架
	pthread_mutex_lock(&(pPool->taskLock)); 		//上锁互斥量
	//判断当前是否有空闲的工作线程,如果不判断会导致条件变量信号丢失问题
	while(pPool->busyWork > POOL_NUM){
		pthread_mutex_unlock(&(pPool->taskLock));
		usleep(1000);
		pthread_mutex_lock(&(pPool->taskLock));
	}
	if((pTask = malloc(sizeof(Task))) == NULL){ 	//生产资源
		printf("malloc err\n");
		return;
	}
	pTask->func = pfun;
	pTask->arg = arg;
	pTask->pNext = NULL;
	pPool->queue_tail->pNext = pTask;
	pPool->queue_tail = pTask;
	pPool->busyWork++; 								//资源生产结束,代表将有一个线程会工作
	pthread_cond_signal(&(pPool->newTask)); 		//发送信号
	pthread_mutex_unlock(&(pPool->taskLock)); 		//解锁互斥量
}

/*
 * workThread:工作线程
 * */
void* workThread(void* arg){
	Task* pTask = NULL;
	while(1){
		//消费者代码框架
		pthread_mutex_lock(&(pPool->taskLock)); 	//上锁互斥量
		while(pPool->queue_head->pNext == NULL){    //等待资源
			pthread_cond_wait(&(pPool->newTask),&(pPool->taskLock));
		}
		pTask = pPool->queue_head->pNext; 			//使用资源
		pPool->queue_head->pNext = pTask->pNext;
		if(pPool->queue_head->pNext == NULL){
			pPool->queue_tail = pPool->queue_head;
		}
		pthread_mutex_unlock(&(pPool->taskLock));   //解锁互斥量
		//资源中的任务与消费者无关,因此可以放在互斥量外
		pTask->func(pTask->arg); 					//执行相应任务函数
		free(pTask);
		pPool->busyWork--; 							//当前线程工作完成
	}
}

/*
 * pool_init:线程池初始化
 * @ret -1--err 0--success
 */
int pool_init(void){

	//1.申请空间
	if((pPool = malloc(sizeof(ThreadPool))) == NULL){
		printf("malloc err\n");
		return -1;
	}
	//2.初始化
	pthread_mutex_init(&(pPool->taskLock),NULL); 			//互斥量初始化
	pthread_cond_init(&(pPool->newTask),NULL); 				//条件变量初始化
	pPool->busyWork = 0;									//当前工作的线程=0
	if((pPool->queue_head = malloc(sizeof(Task))) == NULL){ //任务队列初始化
		printf("malloc err\n");
		return -1;
	}
	pPool->queue_tail = pPool->queue_head;
	pPool->queue_head->pNext = NULL;
	for(int i=0;i<POOL_NUM;i++){ 							//创建工作线程
		if(pthread_create(&(pPool->tid[i]),NULL,workThread,NULL) != 0){
			perror("pthread_create");
			return -1;//这里没有做清理工作
		}
	}
	return 0;
}

import requests import time from bs4 import BeautifulSoup import os import csv import queue import threading import random from concurrent.futures import ThreadPoolExecutor from fake_useragent import UserAgent import logging # 配置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', filename='crawler.log' ) # 全局变量 csv_lock = threading.Lock() header_written = False """生成待爬取的URL队列""" def getUrl(): urlQueue = queue.Queue() for i in range(0, 180, 20): # 0-160,每页20条,共9页 url = f"https://movie.douban.com/subject/36053256/comments?start={i}&limit=20&status=P&sort=new_score" urlQueue.put(url) return urlQueue """从URL队列获取内容并解析,带重试机制""" def getContent(urlQueue, max_retries=3): while True: try: # 使用timeout参数避免永久阻塞 url = urlQueue.get(timeout=5) except queue.Empty: logging.info("URL队列已空,退出内容获取线程") return retries = 0 while retries < max_retries: try: # 添加随机延迟,避免频繁请求 time.sleep(random.uniform(2, 5)) # 发送请求 html = requests.get(url, headers=getHeaders(), timeout=15) html.raise_for_status() # 检查HTTP状态码 # 检查是否被反爬 if "检测到有异常请求" in html.text: raise Exception("触发反爬机制") htmlValue = BeautifulSoup(html.text, 'html.parser') articles = htmlValue.select('.comment-item') # 验证是否获取到评论 if not articles: raise Exception("未找到评论元素,可能页面结构变化或被反爬") # 解析评论 comments = [] for article in articles: try: comment_info = article.select_one('span.comment-info') if not comment_info: continue # 提取用户名 user_name = comment_info.select_one('a').get_text(strip=True) if comment_info.select_one( 'a') else "匿名用户" # 提取评论时间 time_elem = comment_info.select_one('span.comment-time') comment_time = time_elem.get_text(strip=True) if time_elem else "未知时间" # 提取评论地点 location_elem = comment_info.select_one('span.comment-location') comment_place = location_elem.get_text(strip=True) if location_elem else "未知地区" # 提取评分 rate_elem = comment_info.select_one('span[class*="allstar"].rating[title]') comment_rate = rate_elem['title'] if rate_elem else "未评分" # 提取评论内容 content_elem = article.select_one('span.short') comment_content = content_elem.get_text(strip=True) if content_elem else "无内容" comments.append([user_name, comment_time, comment_place, comment_rate, comment_content]) except Exception as e: logging.error(f"解析评论时出错: {e}") continue # 批量保存评论 if comments: downloaderContent(comments) logging.info(f"成功处理页面: {url}, 获取到 {len(comments)} 条评论") break except requests.exceptions.RequestException as e: retries += 1 logging.warning(f"请求失败 ({url}): {e},尝试重试 ({retries}/{max_retries})") time.sleep(5 * retries) # 指数退避 except Exception as e: retries += 1 logging.error(f"处理页面时出错 ({url}): {e},尝试重试 ({retries}/{max_retries})") time.sleep(5 * retries) # 指数退避 else: logging.error(f"达到最大重试次数,跳过URL: {url}") urlQueue.task_done() # 标记任务完成 """生成随机请求头,增强反爬能力""" def getHeaders(): try: ua = UserAgent() return { 'User-Agent': ua.random, 'Referer': 'https://movie.douban.com/', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Connection': 'keep-alive', 'Cache-Control': 'max-age=0', 'TE': 'Trailers' } except Exception: # 备用User-Agent列表 user_agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11.5; rv:90.0) Gecko/20100101 Firefox/90.0' ] return { 'User-Agent': random.choice(user_agents), 'Referer': 'https://movie.douban.com/' } """线程安全的CSV写入,批量处理""" def downloaderContent(content): global header_written try: src = './qimocontent/' setDir(src) filename = os.path.join(src, 'douban_comments.csv') with csv_lock: file_exists = os.path.isfile(filename) with open(filename, 'a', newline='', encoding='utf-8-sig') as writer: f_csv = csv.writer(writer) if not file_exists or not header_written: f_csv.writerow(['用户名', '发表时间', '发表地点', '评级', '评论']) header_written = True f_csv.writerows(content) except Exception as e: logging.error(f"保存数据失败: {e}") """创建目录,增加异常处理""" def setDir(dirname): try: if not os.path.exists(dirname): os.makedirs(dirname) except Exception as e: logging.error(f"创建目录失败: {dirname}, 错误: {e}") """主函数,使用线程池管理并发""" def main(): start_time = time.time() logging.info("豆瓣评论爬虫启动...") try: # 初始化队列 url_queue = getUrl() # 使用线程池控制并发 with ThreadPoolExecutor(max_workers=3) as executor: for _ in range(3): # 创建3个工作线程 executor.submit(getContent, url_queue) # 等待所有任务完成 url_queue.join() logging.info(f"爬取完成!耗时: {time.time() - start_time:.2f}秒") logging.info(f"数据已保存到: ./qimocontent/douban_comments.csv") print(f"爬取完成!耗时: {time.time() - start_time:.2f}秒") print(f"数据已保存到: ./qimocontent/douban_comments.csv") except KeyboardInterrupt: logging.info("用户中断操作,程序退出") print("程序已被用户中断") except Exception as e: logging.critical(f"主程序异常: {e}", exc_info=True) print(f"程序发生严重错误: {e}") if __name__ == '__main__': main() 这是我对《苦尽甘来遇见你》在豆瓣网上的数据爬取的代码,但是爬取出来的数据有很多重复的,并且爬取到一定数据量后,就不允许爬取了,但我想爬取大约300条数据,可以根据我的要求帮我重新修改代码
06-21
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值