谈这个问题之前,首先必须清楚一个概念,那就是程序切换(CPU时间的分配)。
我们现在使用的windows操作系统,是可以"同时"做很多件事儿的。比如我们可以一边看电影,一边聊QQ;一边听歌,一边打游戏。但是,这所谓的"同时",在操作系统底层可能并不是真正的意义上的"同时"。
实际上,对于单CPU的计算机来说,在CPU中,同一时间是只能干一件事儿的。为了看起来像是“同时干多件事”,Windows这种操作系统是把CPU的时间划分成长短基本相同的时间区间,即”时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个应用使用。这样,给用户的感觉是他在同时的进行听歌和打游戏,实际上,在操作系统中,CPU是在游戏进程和音乐播放器进程之间来回切换执行的。
操作系统时间片的使用是有规则的:某个作业在时间片结束之前,整个任务还没有完成,那么该作业就被暂停下来,放弃CPU,等待下一轮循环再继续做。此时CPU又分配给另一个作业去使用。
我们把目光聚焦在CPU的执行上,把这个过程放大的话,CPU就好像是一个电话亭。多个用户并不是同一时间在使用这个电话亭中的电话的,而是轮流使用的。
由于计算机的处理速度很快,只要时间片的间隔取得适当,那么一个用户作业从用完分配给它的一个时间片到获得下一个CPU时间片,中间有所”停顿”,但用户察觉不出来。
所以,在单CPU的计算机中,我们看起来“同时干多件事”,其实是通过CPU时间片技术,并发完成的。
并发和并行最开始都是操作系统中的概念,表示的是CPU执行多个任务的方式。这两个概念极容易混淆。
并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
分布式(Distributed),是近年提出的一种新的计算方式,所谓分布式计算(集群)就是在两个或多个软件互相共享信息,这些软件既可以在同一台计算机上运行,也可以在通过网络连接起来的多台计算机上运行。
以上都是形而上的概念级定义,具体在计算机里主要实现的手段就是进程(Process),线程(Thread)和协程(Coroutines)。
进程
是执行中的计算机程序。也就是说,每个代码在执行的时候,首先本身即是一个进程。
特性:
- 每个程序,本身首先是一个进程。
- 运行中每个进程都拥有自己的地址空间、内存、数据栈及其它资源。
- 操作系统本身自动管理着所有的进程(不需要用户代码干涉),并为这些进程合理分配可以执行时间。
- 进程可以通过派生新的进程来执行其它任务,不过每个进程还是都拥有自己的内存和数据栈等。
- 进程间可以通讯(发消息和数据),采用进程间通信(IPC) 方式。
其实,多个进程可以在不同的 CPU 上运行,互不干扰。同一个CPU上,可以运行多个进程,由操作系统来自动分配时间片。由于进程间资源不能共享,需要进程间通信,来发送数据,接受消息等。
多进程,广义上也称为“并行”。
线程
是在进程中执行的代码。是处理器调度的基本单位。
一个进程下可以运行多个线程,这些线程之间共享主进程内申请的操作系统资源。在一个进程中启动多个线程的时候,每个线程按照顺序执行。现在的操作系统中,也支持线程抢占,也就是说其它等待运行的线程,可以通过优先级,信号等方式,将运行的线程挂起,自己先运行。
特性:
- 线程,必须在一个存在的进程中启动运行。
- 线程使用进程获得的系统资源,不会像进程那样需要申请CPU等资源。
- 线程无法给予公平执行时间,它可以被其他线程抢占,而进程按照操作系统的设定分配执行时间。
- 每个进程中,都可以启动很多个线程。
多线程,广义上也称”并发“。
协程
又称微线程。
协程是一种用户级的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其它协程共享全局数据和其它资源。
协程是用户自己来编写调度逻辑的,对CPU来说,协程其实是单线程,所以CPU不用去考虑怎么调度、切换上下文,这就省去了CPU的切换开销,所以协程在一定程度上又好于多线程。
协程间是协同调度的,这使得并发量数万以上的时候,协程的性能是远远高于线程。
协程,广义上也当然也是称”并发“。
一般来说,在Python中编写并行/并发程序的经验:
- 计算密集型任务:使用多进程
- IO密集型任务:(如:网络通讯)使用多线程/协程,较少使用多进程。这是由于 IO操作需要独占资源,比如:网络通讯(微观上每次只有一个人说话,宏观上看起来像同时聊天)每次只能有一个人说话文件读写同时只能有一个程序操作(如果两个程序同时给同一个文件写入 ‘a’, ‘b’,那么到底写入文件的哪个呢?)
Python多进程
本文都是以“池”的概念进行概述,笔者认为维护一个“池”实现并发/并行是标准,也极力推荐此方式,故忽略一般教程刚开始就提到实例化两个Process类的类似方式。
- multiprocessing 家族中的Pool,有同步调用(apply()),异步调用(apply_async())和map调用三种方式选择。其中异步调用使用get()方法去取返回的数据,map调用则不需要关注进程的join和close。当然,这个模块是所有改进版的多进程的基石库,其实可以找一篇详细的教程去每种方式都尝试。(较为推荐)
- concurrent.futures.ProcessPoolExecutor 家族,调用方式与multiprocessing 类似。用futures的写法上更简洁一些,concurrent.futures的性能并没有更好,只是让编码变得更简单。使用map时,future是逐个迭代提交,multiprocessing.Pool是批量提交jobs,因此对于大批量jobs的处理,multiprocessing.Pool效率会更高一些。对于需要长时间运行的作业,用future更佳。concurrent.futures.ProcessPoolExecutor是对multiprocessing的封装,在运行时需导入__main__,不能直接在交互窗口工作。(相比不推荐)
- deco,是用于Python的简化并行计算模型。deco自动并行化Python程序,并且只需对现有的串行程序进行最少的修改即可。(推荐)
2021/1/18日更新:请注意以下几点问题帮助你根据当前任务选择合适的多进程使用方法:
- 作者经使用发现deco甚至可以子进程也可以继续再分子进程,但不建议这么做,有出现不好管理进程和父子孙进程嵌套诅咒的可能。
- deco的进程管理有一定缺陷,一个Python脚本如果用到n次deco(不同concurrent函数),系统Python进程数量也会是n*process_num,只有脚本执行完才能释放,这样就有可能增加进程切换负担,可以改进,作者也尽可能尝试优化一下再进行更新。
- 如果进程中非要打印进度条且十分在意命令行运行的美观性的话,不要使用deco库,建议使用 multiprocessing 和 tqdm 结合使用,具体使用方法参考 全栈之后端开发系列 - “轮子” | 依赖库与工具 的第一个轮子详细信息。deco 仅仅就是适合简单的多进程任务且有强烈代码美观偏执症或装饰器偏执症患者。(作者不幸患上了(ಥ﹏ಥ))
deco
威斯康星大学麦迪逊分校的Alex Sherman和Peter Den Hartog在2016年编写了一个新的有趣的Python多处理程序包装,称为deco。该库基于称为Pydron的东西,2016年时有人称这个项目是一项研究,当时并未没有发布任何代码。
论文:
DECO: Polishing Python Parallel Programmingdrive.google.com项目托管在GitHub:
decogithub.comdeco使用Python装饰器来更改功能,并使它们同时执行或并行执行。它包括两个装饰器以促进并发编程。使用deco的第一步是确定一个应并行执行的函数,并将装饰器应用于该函数@concurrent。使用deco的第二步是标记@concurrent使用第二个装饰器调用函数的所有函数@synchronized。标@synchronized有的@concurrent函数将被修改,以确保所有对函数的调用都适当同步,并正确处理了它们的结果。
除了在函数上使用简单的装饰器之外,最大的不同在于deco它真的很容易上手,并且对如何收集子流程调用的结果也有严格的限制。在deco中,您传入具有键索引(例如python )的可变对象dict。python list 也是可变的,但没有索引。意思是,您可以获取处理信息通过mylist.append()。这是在Python中同时运行代码的简化方法。由于CPython全局解释器锁,运行Python中的并行代码当前需要使用多个独立的进程,并对它们的函数调用和参数进行序列化和管道化。
“但是,DECO确实对该程序施加了一个重要限制:所有突变都只能基于索引。”
以下是这两个装饰器的示例用法,这将是deco多进程的标准理想用例。
import deco
import time
from collections import defaultdict
@deco.concurrent(processes=16)
def each_xxxx_instance(lat, lon, data):
"""
We add this for the concurrent function
:param lat:
:param lon:
:param data:
:return:
"""
time.sleep(0.1)
return data[lat + lon]
@deco.synchronized
def xxxx(data):
"""
And we add this for the function which calls the concurrent function
:param data:
:return:
"""
results = defaultdict(dict)
for lat in range(10):
for lon in range(10):
results[lat][lon] = each_xxxx_instance(lat, lon, data)
return dict(results)
# Windows下必须在__main__命名下,所以如果调用,也都遵循这个规则吧
if __name__ == "__main__":
xxxx(data)
程序员唯一干预就是插入两个装饰器@concurrent和@synchronized。在@concurrent你想一个函数来并行运行标识及@synchronized装饰标记的功能,其中并发功能将被调用。
为什么说deco强大,真正的理由是这样的:
我们可以修改并发函数中的参数,并且修改的内容同步回父进程。这不同于Python的multiprocess.pool,后者要求从并发函数中返回任何已更改的状态,并丢弃对参数的修改。
DECO主要只是Python的multiprocessing.pool的智能包装。当@concurrent
应用于函数时,它将替换为对pool.apply_async的调用。此外,当将参数传递给pool.apply_async时,DECO会将所有索引可变对象替换为代理,从而使其能够检测并同步这些对象的突变。然后,可以通过在并发函数上调用wait()并调用同步事件来获得这些调用的结果。通过@synchronized
在调用@concurrent
函数的函数上使用装饰器,可以将这些事件自动放入代码中。另外在使用时@synchronized
,您可以直接将并发函数调用的结果分配给索引可变对象。这些分配由DECO重构,以在下一个同步事件期间自动发生。所有这些意味着,在许多情况下,使用DECO进行并行编程的方式与简化串行编程的方式完全相同。
Python多线程
- multiprocessing 家族中.pool里的ThreadPool,调用方式与多进程类似。(较为推荐)
- concurrent.futures.ThreadPoolExecutor家族,调用方式与多进程类似。用futures的写法上更简洁一些,concurrent.futures的性能并没有更好,只是让编码变得更简单。使用map时,future是逐个迭代提交,multiprocessing.Pool是批量提交jobs,因此对于大批量jobs的处理,multiprocessing.Pool效率会更高一些。对于需要长时间运行的作业,用future更佳。concurrent.futures.ProcessPoolExecutor是对multiprocessing的封装,在运行时需导入__main__,不能直接在交互窗口工作。(不推荐)
- threadpool,易于使用的面向对象的线程池框架,笔者用过这个库,能正常使用,但是最后一次更新是2015年,连作者都说:该模块已过时,仅在PyPI上提供,以支持仍在使用它的旧项目。请不要将其用于新项目! (不推荐)
- deco,是用于Python的简化并行计算模型。支持多线程模块。(推荐)
2021/1/18日更新:请注意以下几点问题帮助你根据当前任务选择合适的多线程使用方法:
- 如果进程中非要打印进度条且十分在意命令行运行的美观性的话,不要使用deco库,建议使用 multiprocessing 和 tqdm 结合使用,具体使用方法参考 全栈之后端开发系列 - “轮子” | 依赖库与工具 的第一个轮子详细信息。deco 仅仅就是适合简单的多线程任务且有强烈代码美观偏执症或装饰器偏执症患者。(作者不幸患上了(ಥ﹏ಥ))
以下是这两个装饰器的示例用法,这将是deco多线程的标准理想用例。
import deco
import time
from collections import defaultdict
@deco.concurrent.threaded(processes=16)
def each_xxxx_instance(lat, lon, data):
"""
We add this for the concurrent function
:param lat:
:param lon:
:param data:
:return:
"""
time.sleep(0.1)
return data[lat + lon]
@deco.synchronized
def xxxx(data):
"""
And we add this for the function which calls the concurrent function
:param data:
:return:
"""
results = defaultdict(dict)
for lat in range(10):
for lon in range(10):
results[lat][lon] = each_xxxx_instance(lat, lon, data)
return dict(results)
# Windows下必须在__main__命名下,所以如果调用,也都遵循这个规则吧
if __name__ == "__main__":
xxxx(data)
其中计算线程数抽象通用公式是:
能让CPU的利用率最大化。
TC是工作线程数(线程池线程数);N核服务器;通过执行业务的单线程分析出本地计算时间为T1,等待时间为T2。
Python协程
- yield, Python通过yield提供了对协程的基本支持,但是不完善。
- gevent,为Python提供了比较完善的协程支持,是第三方库,通过greenlet实现协程。
Python分布式
Python中的 multiprocessing 包含一种在多个CPU上运行Python工作负载的本地方法。但是有时候甚至 multiprocessing 还不够。有时,这项工作要求不仅在多个内核之间而且还要在多个计算机之间分配工作。
Ray
由加利福尼亚大学伯克利分校的一组研究人员开发的Ray是许多分布式机器学习库的基础。但是Ray不仅限于机器学习任务,即使那是它的原始用例。使用Ray,可以分解任何Python任务并将其分布在整个系统中。
Ray的语法非常简单,因此您无需大量修改现有应用程序即可对其进行并行化。所述@ray.remote
装饰分布到任何可用的节点该函数在一个雷群集,任选指定的参数对于许多CPU或GPU如何使用。每个分布式函数的结果都作为Python对象返回,因此它们易于管理和存储,并且跨节点或节点内部的复制量保持最小。例如,在处理NumPy数组时,此最后一个功能非常有用。
Ray甚至包括其自己的内置群集管理器,该管理器可以根据需要在本地硬件或流行的云计算平台上自动启动节点。
本文为2017/12/7的《Python中理解进程(Process),线程(Thread)和协程(Coroutines)的感悟》和2019/11/14/的《带装饰器的Python中的简化多进程、多线程并发(装饰并发-Python多线程、进程神器)》博客迁移,并进行更新,日后在此继续更新...
赞助
声明
本博客属个人所有,不涉及商业目的。遵守中华人民共和国法律法规、中华民族基本道德和基本网络道德规范,尊重有节制的言论自由和意识形态自由,反对激进、破坏、低俗、广告、投机等不负责任的言行。所有转载的文章、图片仅用于说明性目的,被要求或认为适当时,将标注署名与来源。若不愿某一作品被转用,请及时通知本人。对于无版权或自由版权作品,本博客有权进行修改和传播,一旦涉及实质性修改,本博客将对修改后的作品享有相当的版权。二次转载者请再次确认原作者所给予的权力范围。
本博客所有原创作品,包括文字、资料、图片、网页格式,转载时请标注作者与来源。非经允许,不得用于赢利目的。本博客受中国知识产权、互联网法规和知识共享条例保护和保障,任何人不得进行旨在破坏或牟取私利的行为。