之前多线程用 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》中也有更简单的定义。并发是指一次处理多件事,并行是指一次做多件事,相对而言,并行更加强调同时。