背景
1、在构建模型池之后需要对大量的数据进行批更新和同步,在这个过程当中会产生大量的请求访问和数据返回,并且由于数据之间的独立性。并不需要在循环当中等待每一个数据源实现数据的返回,可以发送多个请求进行同步接收,因此可以通过多线程的方式提升程序的操作速度。
2、如何监控线程池当中线程的利用效率和cpu的载荷情况?
3、由于每次都需要将任务的func函数存在for循环的位置重构为多线程方式,有没有更便捷的方式来解决这个问题?
问题1解决方案
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
import io
from PyPDF2 import PdfReader
import logging
# 定义你的任务函数,例如下载和读取PDF
def task_function(url, *args, **kwargs):
try:
response = requests.get(url)
response.raise_for_status()
pdf_stream = io.BytesIO(response.content)
pdf_reader = PdfReader(pdf_stream)
text = ' '.join(page.extract_text() for page in pdf_reader.pages)
# 处理text为空的情况,例如跳过或记录日志
if not text:
logging.warning(f"No text found in PDF at {url}")
return url, None
return url, text
except requests.RequestException as e:
logging.error(f"Error downloading {url}: {e}")
except Exception as e:
logging.error(f"Error reading {url}: {e}")
# 多线程执行任务的模板函数
def execute_concurrent_tasks(task_list, max_workers=10):
results = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 使用字典推导式创建futures字典
futures = {executor.submit(task_function, url): url for url in task_list}
# 迭代futures字典的as_completed结果
for future in as_completed(futures):
url = futures[future]
try:
# 处理正常结果
result = future.result()
if result[1] is not None: # 确保text不为空
results[url] = result[1]
logging.info(f"Processed {url}")
except Exception as e:
# 记录错误信息
logging.error(f"Error processing {url}: {e}")
logging.info(f"Total processed tasks: {len(results)}")
return results
# 示例使用
if __name__ == "__main__":
file_list = ['http://example.com/pdf1.pdf', 'http://example.com/pdf2.pdf'] # 你的任务列表
all_results = execute_concurrent_tasks(file_list)
这个模板展示了如何定义一个任务函数,如何使用ThreadPoolExecutor来并发执行这些任务,并如何收集结果。task_function是您需要并发执行的具体任务,例如下载和读取PDF。execute_concurrent_tasks函数是执行这些任务的模板,它接受任务列表和最大工作线程数,返回一个包含结果的字典。
请注意,这个模板使用了logging库来记录信息和错误,您需要根据实际情况配置日志记录。此外,您可能需要根据任务的具体情况调整task_function和错误处理逻辑。
值得注意的是,你定义了一个results [dict]作为返回值的处理和存放方式,而且全都是在写入过程当中,在url完全不一致的情况下可能不会出现任何问题。但是如果需要让这个代码有更强的普适性,我们需要思考,python当中的字典、DataFrame以及对数据库的同步操作当中,这些数据结构是否是线程安全的?
答案:二者都不是线程安全的,都会发生脏读脏写的问题,因此需要保证线程安全。
改进方式,在必要的位置增加lock锁,保证读写的线程是唯一的。
from threading import Lock
lock = Lock()
results = {}
with ThreadPoolExecutor() as executor:
futures = {executor.submit(download_and_read_pdf, pdf_url): pdf_url for pdf_url in file_list}
for future in as_completed(futures):
try:
url, text = future.result()
if text:
with lock: # 使用锁来同步写操作
results[url] = text
logging.info(f"Processed {url}")
except Exception as e:
logging.error(f"Error processing {url}: {e}")
读写操作:在多线程环境中,对DataFrame进行读写操作(如修改数据、添加列等)需要同步,以避免数据竞争。
数据复制:在将DataFrame传递给线程之前,通常需要对其进行复制,以确保每个线程操作的是数据的独立副本。
使用锁:可以使用threading.Lock或其他同步机制来确保在任何给定时间只有一个线程可以修改DataFrame。
局部数据:每个线程可以操作自己的局部DataFrame,然后在主线程中合并结果。
并行处理:对于大规模数据处理,可以使用pandas的concat函数将多个线程生成的局部DataFrame合并为一个。
使用concurrent.futures:虽然DataFrame本身不是线程安全的,但可以使用concurrent.futures来管理线程池,并在每个线程中操作独立的数据副本。
对于DataFrame的线程安全锁示例如下:
import pandas as pd
from threading import Thread, Lock
# 创建一个共享的DataFrame和锁
shared_df = pd.DataFrame()
lock = Lock()
# 定义线程工作函数
def thread_function(data):
global shared_df
# 在写入之前获取锁
with lock:
# 这里可以对shared_df进行操作
local_df = pd.DataFrame(data)
shared_df = pd.concat([shared_df, local_df])
# 创建线程列表
threads = []
data_list = [[1, 2], [3, 4], [5, 6]] # 假设这是线程要处理的数据列表
# 创建并启动线程
for data in data_list:
thread = Thread(target=thread_function, args=(data,))
threads.append(thread)
thread.start()
# 等待所有线程完成
for thread in threads:
thread.join()
print(shared_df)
上述示例使用了一个全局的DataFrame和Lock来确保在多线程环境中对DataFrame的操作是线程安全的。每个线程在修改共享DataFrame之前都会获取锁,并在操作完成后释放锁。
但是当涉及到大规模数据处理时,可能需要考虑使用其他并行处理技术或分布式计算框架,如Dask或Spark,这些框架专门设计用于处理并行数据操作,也能够减少很多的个人工作量。
问题2解决方案
在Python中,使用标准库中的concurrent.futures.ThreadPoolExecutor和as_completed可以创建和管理线程池,但它们本身并不提供直接监控CPU负载或线程负载的功能。不过,可以使用其他库来获取这些信息。
以下是一些可以用来监控CPU和线程负载的库:
psutil:一个跨平台库,用于获取系统利用率信息(如CPU、内存、磁盘、网络等)以及系统和应用程序的运行进程和线程。
os和sys:Python标准库中的模块,可以用来获取一些系统级别的信息。
threading:Python标准库中的模块,可以用来监控线程的运行状态。
首先,你需要安装psutil库(如果尚未安装):
pip install psutil
然后,你可以使用以下代码来监控CPU负载和线程状态:
import psutil
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
# 定义一些任务函数
def task_function(x):
time.sleep(1) # 模拟耗时的操作
return x * x
# 使用 ThreadPoolExecutor 运行任务
def main():
tasks = [(task_function, (i,)) for i in range(5)] # 创建任务列表
with ThreadPoolExecutor(max_workers=5) as executor:
future_to_task = {executor.submit(*task): task for task in tasks}
while future_to_task:
# 轮询线程池的状态
for future in as_completed(future_to_task):
task = future_to_task.pop(future)
try:
result = future.result()
except Exception as exc:
print(f'任务发生异常: {exc}')
else:
print(f'任务结果: {result}')
# 打印CPU负载
cpu_load = psutil.cpu_percent()
print(f"当前CPU负载: {cpu_load}%")
# 打印所有线程的CPU时间
for proc in psutil.process_iter(['pid', 'name', 'cpu_times']):
print(proc)
time.sleep(0.1) # 短暂休眠以避免过度CPU使用
if __name__ == "__main__":
main()
请注意,频繁地检查CPU负载或线程状态可能会对性能产生影响,因此你可能需要在实际监控逻辑和性能开销之间找到平衡。
此外,psutil提供的cpu_percent函数可以获取当前CPU的负载百分比,而process_iter函数可以迭代系统中的所有进程,并获取它们的信息,包括CPU时间。在这个示例中,我们简单地打印了CPU负载和系统中每个进程的CPU时间。可以根据需要调整这些信息的收集和报告方式。
问题3解决方案
代码中使用了concurrent.futures.ThreadPoolExecutor来执行并发任务,并通过as_completed来迭代完成的任务。为了使代码更加方便和可重用,我们可以使用装饰器来创建一个通用的并发执行函数。装饰器可以减少代码重复并使任务的并发执行更加灵活。
使用装饰器优化多线程代码
装饰器是一种设计模式,用于修改或增强函数的行为。对于多线程任务,我们可以创建一个装饰器来自动处理任务的提交和结果的收集。
以下是一个简单的装饰器示例,用于自动执行并发任务并收集结果:
from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import wraps
def concurrent_task(max_workers=10):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 假设args是任务列表
task_list = args[0]
results = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(func, *task_args): task for task, task_args in zip(task_list, args[1:])}
for future in as_completed(futures):
task = futures[future]
try:
result = future.result()
results[task] = result
except Exception as e:
print(f"Error processing task {task}: {e}")
return results
return wrapper
return decorator
# 使用装饰器
@concurrent_task(max_workers=5)
def task_function(*args, **kwargs):
# 这里是具体的任务实现
pass
# 示例使用
if __name__ == "__main__":
tasks = [(task_args1), (task_args2), ...] # 任务参数列表
results = task_function(tasks)
print(results)
为什么使用装饰器可能不理想
1、灵活性问题:装饰器可能会限制函数的灵活性,因为它要求函数接受特定的参数形式,这可能不适用于所有情况。
2、调试困难:使用装饰器可能会使调试变得更加困难,因为堆栈跟踪可能不会直接指向原始函数。
3、性能开销:装饰器可能会引入额外的性能开销,尤其是在高并发场景下。
4、复杂性增加:对于复杂的并发任务,装饰器可能无法提供足够的控制,需要更精细的并发控制策略。 其他方案
如果装饰器方案不理想,我们可以考虑以下替代方案:
1、使用函数式编程:使用map或starmap函数结合ThreadPoolExecutor来简化并发任务的执行。
2、使用类封装:创建一个类来封装并发任务的逻辑,提供方法来添加任务、启动执行和获取结果。
3、使用高级并发库:使用如joblib或Dask等高级并发库,它们提供了更丰富的并发控制和性能优化功能。 注: joblib的并行效果要优于pickle。对比实验
选择哪种方案取决于具体的应用场景和性能要求。在实际开发中,应该根据任务的复杂性、并发需求和性能目标来选择最合适的方法。