Python 之 concurrent.futures 使用

之前多线程用 threading 的时候比较多,最近看看 concurrent.futures 实现多线程多进程的方法。

ThreadPoolExecutor

ThreadPoolExecutor 适合对于I/O密集的任务,比如如网络请求、文件操作等。

多线程中 print 打印换行有点乱,这是因为 print 打印 value 和 end 换行符的操作不是原子的,多线程的时候可能中间还会穿插其他打印操作。所以我们自己手动在 value 加上换行符就可以了。

不阻塞

import datetime
import time
from concurrent import futures


def run(n):
    # print(f"{datetime.datetime.now()} 线程 Thread-{n} 开始执行")
    time.sleep(3)
    print(f"{datetime.datetime.now()} 线程 Thread-{n} 执行结束\n", end='')


if __name__ == '__main__':
    print(f"{datetime.datetime.now()}  主线程开始执行\n", end='')
    all_task = []
    executor = futures.ThreadPoolExecutor(max_workers=3)
    for i in range(1, 8):
        task = executor.submit(run, i)
        all_task.append(task)
    print(f"{datetime.datetime.now()}  主线程结束执行\n", end='')
2024-12-13 11:26:33.465840  主线程开始执行
2024-12-13 11:26:33.469839  主线程结束执行
2024-12-13 11:26:36.469357 线程 Thread-1 执行结束
2024-12-13 11:26:36.470359 线程 Thread-3 执行结束
2024-12-13 11:26:36.470359 线程 Thread-2 执行结束
2024-12-13 11:26:39.469776 线程 Thread-4 执行结束
2024-12-13 11:26:39.470677 线程 Thread-5 执行结束
2024-12-13 11:26:39.471319 线程 Thread-6 执行结束
2024-12-13 11:26:42.470184 线程 Thread-7 执行结束

阻塞

使用 with ThreadPoolExecutor 时,看源码可知 executor.__exit__ 方法会调用 executor.shutdown(wait=True) 方法,它会在所有线程都执行完毕前阻塞线程。

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.shutdown(wait=True)
        return False
import datetime
import time
from concurrent import futures


def run(n):
    # print(f"{datetime.datetime.now()} 线程 Thread-{n} 开始执行")
    time.sleep(3)
    print(f"{datetime.datetime.now()} 线程 Thread-{n} 执行结束\n", end='')


if __name__ == '__main__':
    print(f"{datetime.datetime.now()}  主线程开始执行\n", end='')
    with futures.ThreadPoolExecutor(max_workers=3) as executor:
        all_task = executor.map(run, range(1, 8))
    print(f"{datetime.datetime.now()}  主线程结束执行\n", end='')
2024-12-13 11:20:14.092288  主线程开始执行
2024-12-13 11:20:17.095038 线程 Thread-1 执行结束
2024-12-13 11:20:17.096037 线程 Thread-2 执行结束
2024-12-13 11:20:17.096227 线程 Thread-3 执行结束
2024-12-13 11:20:20.095460 线程 Thread-4 执行结束
2024-12-13 11:20:20.096544 线程 Thread-5 执行结束
2024-12-13 11:20:20.096544 线程 Thread-6 执行结束
2024-12-13 11:20:23.095876 线程 Thread-7 执行结束
2024-12-13 11:20:23.095876  主线程结束执行

当然,我们也可以不用 with ,然后手动调用 wait 方法按指定条件进行阻塞也是一样。

import datetime
import time
from concurrent import futures


def run(n):
    # print(f"{datetime.datetime.now()} 线程 Thread-{n} 开始执行")
    time.sleep(3)
    print(f"{datetime.datetime.now()} 线程 Thread-{n} 执行结束\n", end='')


if __name__ == '__main__':
    print(f"{datetime.datetime.now()}  主线程开始执行\n", end='')
    all_task = []
    executor = futures.ThreadPoolExecutor(max_workers=3)
    for i in range(1, 8):
        task = executor.submit(run, i)
        all_task.append(task)
    futures.wait(all_task, return_when=futures.FIRST_COMPLETED)
    print(f"{datetime.datetime.now()}  主线程结束执行\n", end='')
2024-12-13 11:35:50.043853  主线程开始执行
2024-12-13 11:35:53.047452 线程 Thread-1 执行结束
2024-12-13 11:35:53.047452  主线程结束执行
2024-12-13 11:35:53.048664 线程 Thread-3 执行结束
2024-12-13 11:35:53.048664 线程 Thread-2 执行结束
2024-12-13 11:35:56.048256 线程 Thread-4 执行结束
2024-12-13 11:35:56.049267 线程 Thread-6 执行结束
2024-12-13 11:35:56.049267 线程 Thread-5 执行结束
2024-12-13 11:35:59.048616 线程 Thread-7 执行结束

as_completed

futures.as_completed 在并发执行多个任务的情况下。它可以帮助我们在任务完成时立即处理结果,而不需要等待所有任务都完成。显然,此时再用 futures.wait 的话显然就不应该了。

我们甚至可以依赖这个特性做一个任务处理的实时进度条:

import datetime
import random
import time
import sys
from concurrent import futures


def run(n):
    print(f"{datetime.datetime.now()} 任务 Task-{n} 开始执行\n", end='')
    time.sleep(random.randint(2, 10))
    return f"Task-{n}"


if __name__ == '__main__':
    print(f"{datetime.datetime.now()} 主任务开始执行\n", end='')
    all_task = []
    executor = futures.ThreadPoolExecutor(max_workers=7)
    for i in range(1, 8):
        task = executor.submit(run, i)
        all_task.append(task)
    # futures.wait(all_task, return_when=futures.ALL_COMPLETED)
    future_iter = futures.as_completed(all_task)
    total_count = len(all_task)
    i = 0
    for future in future_iter:
        i += 1
        percent = "{:.0%}".format(i / total_count)
        length = int(percent.replace("%", ""))
        task_name = future.result()
        s = "\r处理进度 %s>%3s" % ("=" * length, percent)  # \r表示回车但是不换行,利用这个原理进行百分比的刷新
        sys.stdout.write(s)  # 向标准输出终端写内容
        sys.stdout.flush()  # 立即将缓存的内容刷新到标准输出
        # print(f"{datetime.datetime.now()} 任务 {task_name} 执行结束\n", end='')
    print()
    print(f"{datetime.datetime.now()} 主任务结束执行\n", end='')
2024-12-13 15:58:32.919201 主任务开始执行
2024-12-13 15:58:32.922109 任务 Task-1 开始执行
2024-12-13 15:58:32.922109 任务 Task-2 开始执行
2024-12-13 15:58:32.923108 任务 Task-3 开始执行
2024-12-13 15:58:32.923108 任务 Task-4 开始执行
2024-12-13 15:58:32.923108 任务 Task-5 开始执行
2024-12-13 15:58:32.924107 任务 Task-6 开始执行
2024-12-13 15:58:32.924107 任务 Task-7 开始执行
处理进度 ====================================================================================================>100%
2024-12-13 15:58:41.924904 主任务结束执行

ProcessPoolExecutor

ProcessPoolExecutor 使用的是多进程,而不是多线程,因此它更适合处理 CPU 密集型任务。至于提供的方法方面,和 ThreadPoolExecutor 基本上是保持一致的。

import datetime
import time
from concurrent import futures


def run(n):
    # print(f"{datetime.datetime.now()} 任务 Thread-{n} 开始执行")
    time.sleep(3)
    print(f"{datetime.datetime.now()} 任务 Task-{n} 执行结束\n", end='')
    return n


if __name__ == '__main__':
    print(f"{datetime.datetime.now()} 主任务开始执行\n", end='')
    all_task = []
    executor = futures.ProcessPoolExecutor(max_workers=3)
    for i in range(1, 8):
        task = executor.submit(run, i)
        all_task.append(task)
    futures.wait(all_task, return_when=futures.ALL_COMPLETED)
    print(f"{datetime.datetime.now()} 主任务结束执行\n", end='')
2024-12-13 12:12:18.384247 主任务开始执行
2024-12-13 12:12:21.493002 任务 Task-1 执行结束
2024-12-13 12:12:21.494054 任务 Task-2 执行结束
2024-12-13 12:12:21.494054 任务 Task-3 执行结束
2024-12-13 12:12:24.493574 任务 Task-4 执行结束
2024-12-13 12:12:24.495136 任务 Task-6 执行结束
2024-12-13 12:12:24.495136 任务 Task-5 执行结束
2024-12-13 12:12:27.494583 任务 Task-7 执行结束
2024-12-13 12:12:27.494583 主任务结束执行

ThreadPoolExecutor 的多线程相比,ProcessPoolExecutor 可以做到多进程真正并行(不是并发),从而来绕过 全局解释器锁的限制。但是进程池切换进程之间存在更大的消耗,因此在可以使用线程池时就尽量使用线程池。 

并发和并行的联系与区别可参考:并行和并发有什么区别_并发和并行的区别-CSDN博客 ,《流畅的Python》中也有更简单的定义。并发是指一次处理多件事,并行是指一次做多件事,相对而言,并行更加强调同时。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值