项目开发--线程池IO操作以及线程安全保证

背景

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。对比实验

选择哪种方案取决于具体的应用场景和性能要求。在实际开发中,应该根据任务的复杂性、并发需求和性能目标来选择最合适的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值