随着Python 3的问世,关于“异步性”和“并发性”的讨论越来越多,我们可以假设Python最近引入了这些功能/概念。 但是事实并非如此。 我们已经多次使用这些操作。 此外,初学者可能会认为asyncio是重新创建和使用异步/并行操作的唯一或最佳方法。 在本文中,我们将探讨实现并行性的各种方法,它们的优缺点。
术语的定义:
在深入研究技术方面之前,对这种情况下经常使用的术语有一些基本的了解是很重要的。
同步和异步:在
同步操作中,任务是一个接一个地执行的。 在
异步任务中,可以彼此独立地启动和完成任务。 当执行移至新任务时,一个异步任务可以启动并继续运行。 异步任务
不会阻塞(不要强制等待任务完成)操作,并且通常在后台执行。
例如,您应该联系旅行社以计划您的下一个假期。 您需要在飞行前给主管写一封信。 在同步模式下,您首先致电旅行社,如果要求您等待,您将等待他们回答您。 然后,您将开始给领导写一封信。 因此,您一个接一个地完成任务。
[同步执行,大约 译者]但是,如果您很聪明,他们会要求您等待
[挂断电话,大约。 译者],您将开始写电子邮件,当您再次讲话时,您将暂停书写,交谈,然后添加字母。 您也可以要求朋友致电代理机构并亲自写信。 这是异步的,任务不会相互阻塞。
竞争力和并发性:竞争能力意味着两项任务是
共同执行的。 在前面的示例中,当我们考虑异步示例时,我们逐渐写了一封信,然后进行了与导游的对话。 代理商。 这就是
竞争力 。
当我们要求打电话给朋友并亲自写信时,这些任务是
并行进行的 。
并发本质上是竞争的一种形式。 但是并发依赖于硬件。 例如,如果CPU只有一个内核,则不能并行执行两个任务。 他们只是在彼此之间共享处理器时间。 这是竞争,但不是并发。 但是,当我们有多个核心时
[作为上一个示例中的朋友,这是第二个核心时,大约为。 我们可以同时执行多个操作(取决于内核数)。
总结一下:
- 同步:阻止操作(阻止)
- 异步:不阻止操作(非阻止)
- 竞争能力:共同进步(共同)
- 并发:并行进度(并行)
并发意味着竞争。 但是竞争并不总是意味着并发。
线程和进程
Python支持线程已经很长时间了。 线程使您可以有竞争力地执行操作。 但是
全局解释器锁(GIL)存在问题,因为线程无法提供真正的并发。 但是,随着
多处理技术的出现
,您可以使用Python使用多个内核。
线程数考虑一个小例子。 在以下代码中,
辅助函数将异步并同时在多个线程上执行。
import threading import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker {}, I slept for {} seconds".format(number, sleep)) for i in range(5): t = threading.Thread(target=worker, args=(i,)) t.start() print("All Threads are queued, let's see when they finish!")
这是一个示例输出:
$ python thread_test.py All Threads are queued, let's see when they finish! I am Worker 1, I slept for 1 seconds I am Worker 3, I slept for 4 seconds I am Worker 4, I slept for 5 seconds I am Worker 2, I slept for 7 seconds I am Worker 0, I slept for 9 seconds
因此,我们启动了5个线程进行协作,并且在它们启动之后(即,在启动worker函数之后),该操作
不会等待线程完成才继续进行下一个print语句。 这是一个异步操作。
在我们的示例中,我们将该函数传递给Thread构造函数。 如果需要,可以使用方法(OOP样式)实现子类。
进一步阅读:要了解有关流的更多信息,请使用下面的链接:
全局翻译锁定(GIL)引入GIL是为了简化CPython的内存处理并提供与C的最佳集成(例如,扩展)。 当Python解释器一次只运行一个线程时,GIL是一种锁定机制。 即 一次只能以Python字节码执行一个线程。 GIL确保不会
并行执行多个线程。
GIL快速详细信息:
- 一个线程可以一次运行。
- Python解释器在线程之间切换以提高竞争力。
- GIL适用于CPython(标准实现)。 但是,例如,Jython和IronPython没有GIL。
- GIL使单线程程序更快。
- GIL通常不会干扰I / O。
- GIL使将线程安全的库轻松集成到C中变得很容易,这归功于GIL,我们拥有许多用C编写的高性能扩展/模块。
- 对于依赖于CPU的任务,解释器每N个滴答声检查一次并切换线程。 因此,一个线程不会阻塞其他线程。
许多人将GIL视为弱点。 我认为这是一种祝福,因为创建了诸如NumPy,SciPy之类的库,它们在科学界中占有特殊而独特的地位。
进一步阅读:这些资源将使您能够深入研究GIL:
工艺流程为了在Python中实现并发,已添加了一个
多处理模块,该模块提供了API,并且如果您之前使用过
线程 ,则看起来非常相似。
让我们去更改前面的示例。 现在,修改后的版本使用
Process而不是
Stream 。
import multiprocessing import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker {}, I slept for {} seconds".format(number, sleep)) for i in range(5): t = multiprocessing.Process(target=worker, args=(i,)) t.start() print("All Processes are queued, let's see when they finish!")
有什么变化? 我只是导入了
多处理模块,而不是
线程 。 然后,我使用一个进程代替了线程。 仅此而已! 现在,我们使用在不同CPU内核上运行的进程来代替许多线程(当然,除非您的处理器有多个内核)。
使用Pool类,我们还可以在多个进程之间为不同的输入值分配一个函数的执行。 官方文档中的示例:
from multiprocessing import Pool def f(x): return x*x if __name__ == '__main__': p = Pool(5) print(p.map(f, [1, 2, 3]))
在这里,我们不是遍历值列表并一次调用一个函数,而是实际上在不同的进程中运行该函数。 一个进程执行f(1),另一个执行f(2),另一个执行f(3)。 最后,结果再次合并到一个列表中。 这使我们可以将繁重的计算分解为较小的部分,并并行运行它们以加快计算速度。
进一步阅读:并发模块parallel.futures模块很大,使编写异步代码非常容易。 我最喜欢的是
ThreadPoolExecutor和
ProcessPoolExecutor 。 这些美术师支持线程或进程池。 我们将任务发送到池,它在可访问的线程/进程中运行任务。 当任务完成时,将返回一个
Future对象,该对象可用于查询和检索结果。
这是ThreadPoolExecutor的示例:
from concurrent.futures import ThreadPoolExecutor from time import sleep def return_after_5_secs(message): sleep(5) return message pool = ThreadPoolExecutor(3) future = pool.submit(return_after_5_secs, ("hello")) print(future.done()) sleep(5) print(future.done()) print(future.result())
我有一篇有关
current.futures masnun.com/2016/03/29/python-a-quick-introduction-to-the-concurrent-futures-module.html的文章。 对于深入研究此模块可能很有用。
进一步阅读:Asyncio-什么,如何以及为什么?
您可能有一个问题,Python社区中的许多人都有-异步带来了什么新东西? 为什么还有另一种使用异步I / O的方法? 我们已经没有线程和进程了吗? 让我们来看看!
为什么我们需要异步?流程非常昂贵
[就资源消耗而言,大约 译者]创建。 因此,对于I / O操作,主要选择线程。 我们知道I / O取决于外部因素-慢速的驱动器或令人不快的网络延迟使I / O经常不可预测。 现在假设我们将线程用于I / O。 3个线程执行各种I / O任务。 口译员将不得不在竞争流程之间进行切换,并依次给每个流程一些时间。 调用流T1,T2和T3。 三个线程开始其I / O操作。 T3首先完成它。 T2和T1仍在等待I / O。 Python解释器正在切换到T1,但仍在等待。 好,解释器移至T2,并且解释器仍在等待,然后移至T3,这已准备就绪并执行代码。 您认为这是一个问题吗?
T3准备好了,但是口译员首先在T2和T1之间切换-这产生了转换成本,如果口译员首先切换到T3,我们可以避免,对吧?
什么是异步?Asyncio为我们提供了一个事件循环以及其他很酷的东西。 事件循环监视I / O事件并切换准备好并等待I / O操作的任务
[事件循环是一种等待到达并在程序中发送事件或消息的软件结构。 翻译者] 。
这个想法很简单。 有一个事件循环。 而且我们具有执行异步I / O的功能。 我们将函数转移到事件循环,并请他为我们运行它们。 事件循环返回给我们一个Future对象,就像一个承诺,将来我们会得到一些东西。 我们坚持一个承诺,不时检查它是否重要(我们真的迫不及待),最后,当收到该值时,我们会在其他一些操作中使用它
[即 我们发送了一个请求,我们立即获得了一张票,并被告知要等到结果出来。 我们会定期检查结果,并在收到结果后立即购票并获得价值。 翻译者] 。
Asyncio使用生成器和协程来停止和恢复任务。 您可以在此处阅读详细信息:
如何使用asyncio?在开始之前,让我们看一个例子:
import asyncio import datetime import random async def my_sleep_func(): await asyncio.sleep(random.randint(0, 5)) async def display_date(num, loop): end_time = loop.time() + 50.0 while True: print("Loop: {} Time: {}".format(num, datetime.datetime.now())) if (loop.time() + 1.0) >= end_time: break await my_sleep_func() loop = asyncio.get_event_loop() asyncio.ensure_future(display_date(1, loop)) asyncio.ensure_future(display_date(2, loop)) loop.run_forever()
请注意,async / await语法仅适用于Python 3.5及更高版本。 让我们看一下代码:
- 我们有一个异步display_date函数,该函数以数字(作为标识符)和事件循环作为参数。
- 该函数具有无限循环,该循环在50秒后中断。 但是在此期间,她反复打印时间并暂停。 等待函数可以等待其他异步函数完成(协程)。
- 我们将该函数传递给事件循环(使用sure_future方法)。
- 我们开始一系列事件。
每当调用await时,asyncio就会意识到该功能可能会花费一些时间。 因此,它暂停执行,开始监视与之关联的所有I / O事件,并允许您运行任务。 当asyncio注意到已暂停的功能I / O已准备就绪时,它将恢复该功能。
做出正确的选择。
我们只是经历了最流行的竞争形式。 但是问题仍然存在-应该选择什么? 这取决于用例。 根据我的经验,我倾向于遵循以下伪代码:
if io_bound: if io_very_slow: print("Use Asyncio") else: print("Use Threads") else: print("Multi Processing")
- CPU限制=>多处理
- I / O绑定,快速I / O,有限的连接数=>多线程
- I / O限制,I / O缓慢,许多连接=> Asyncio
[注意 译者]