并发和并行 | Python中实现多线程 threading 和多进程 multiprocessing
昨天晚上组会轮到我汇报技术内容 ,最近正在和 ray 以及 spark 打交道 ,索性讲一下并发和并行 。反正大家都是管理学院的,平时很少接触这种,因此这个选题不大可能因为内容基础而贻笑大方 。
本文摆一摆并发和并行。附上很简单的 Python 代码 ,涉及到自带库 threading 和 multiprocessing 的使用。
并发和并行
咱们简单用多线程对应并发,多进程对应并行 。多线程并发更强调充分利用性能;多进程并行更强调提升性能上限。
我用非常简单且不那么严谨的比喻来说明。
多线程
一个 CPU 相当于一个学生 。
一个学生一周开一次组会,换句话说一周给老师汇报一次工作。
老师一般会给学生同时布置几个任务 ,比如做比赛、做项目 、读论文,学生可能周一做做比赛 、周二读读论文、周三做做项目... 到了组会,他就把三件事都拿出来汇报 ,老师很欣慰,因为在老师的视角里:学生这三件事是同时在做的。
多线程也是同一个道理,假设你的手机只有一块单核 CPU 。你的 CPU 这 0.01 秒用来播放音乐 ,下 0.01 秒用来解析网页... 在你的视角里:播放音乐和解析网页是同时进行的。你大可以畅快地边听音乐边网上冲浪
何谓充分利用性能? 如果这学生只有一项工作,那他这一周可能只需要花费两天来做任务,剩下时间摸鱼(针不搓 ,三点钟饮茶先!)。因此 ,我们用「多线程」来让学生实现『并发』,充分利用学生能力 。
在实际情况中,多线程、高并发这些词语更多地出现在服务端程序里。比如一个网络连接由一个线程负责 ,一块 CPU 可以负责处理多个异步的请求,大大提升了 CPU 利用率。
多进程
多个 CPU ( CPU 的多核)相当于多个学生 。
一个任务可以拆成几个任务相互协作 、同时进行,则是多进程 。
比如研究生课程 ,老师非得留个论文作业,都研究生了我去,留啥大作业。
那咱就多线程并行搞呗。确定了大概思路 ,剩下的一股脑写就行 。咱队伍里一共甲乙丙丁四名同学,那就:
- 甲同学负责 Introduction
- 乙同学负责 Background
- 丙同学负责 Related Works
- 丁同学负责 Methodology
这是乙同学提出异议:不应该是先完成 Introduction 再写 Background ,一个个来嘛?
大哥 ,都研究生了嗷,作业糊弄糊弄差不多得了啊。让你写你就写。
可以预知,上述四部分同时进行 ,怎么也比一个人写四块要快 。
所以说 多进程并行提升性能上限 。
在实际情况中 ,多进程更多地与高性能计算、分布式计算联系在一起。
Python 实现
首先声明咱的实验环境 。
> python --version Python 3.8.5
咱们设置个任务:求数的欧拉函数值。
def euler_func(n: int) -> int: res = n i = 2 while i <= n // i: if n % i == 0: res = res // i * (i - 1) while (n % i == 0): n = n // i i += 1 if n > 1: res = res // n * (n - 1) return res
求一个数的欧拉函数值可能很快,但是一堆数呢?
所以咱想着用并行完成这个任务。
咱们把任务分成三份 。
task1 = list(range(2, 50000, 3)) # 2, 5, ... task2 = list(range(3, 50000, 3)) # 3, 6, ... task3 = list(range(4, 50000, 3)) # 4, 7, ... def job(task: List): for t in task: euler_func(t)
来看看平平无奇的正常串行。
@timer def normal(): job(task1) job(task2) job(task3)
完成了
task1再完成
task2... 行,没毛病。
看看多线程?
import threading as th @timer def mutlthread(): th1 = th.Thread(target=job, args=(task1, )) th2 = th.Thread(target=job, args=(task2, )) th3 = th.Thread(target=job, args=(task3, )) th1.start() th2.start() th3.start() th1.join() th2.join() th3.join()
再看看多进程?
import multiprocessing as mp @timer def multcore(): p1 = mp.Process(target=job, args=(task1, )) p2 = mp.Process(target=job, args=(task2, )) p3 = mp.Process(target=job, args=(task3, )) p1.start() p2.start() p3.start() p1.join() p2.join() p3.join()
上述代码的逻辑是这样的:
- 我创建线程/进程 ,其生来的目的就是完成任务
job(task1)
或job(task2)
、job(task3)
,注意这里函数名和参数被分开了target=job, args=(task1, )
- 然后
start()
,告诉线程/进程:你可以开始干活了 - 他们自己干自己的 ,咱们程序主逻辑还得继续往下运行
- 到
join()
这里,咱们是指让线程/进程阻塞住咱的主逻辑,比如p1.join()
是指:p1
不干完活 ,我主逻辑不往下进行(属于是「阻塞」) - 这样,我们的函数
multcore
结束后,一定其中的线程/进程任务都完成了
咱看看结果:
if __name__ == '__main__': print("同步串行:") normal() print("多线程并发:") mutlthread() print("多进程并行:") multcore() # 下面是结果 同步串行: timer: using 0.24116 s 多线程并发: timer: using 0.24688 s 多进程并行: timer: using 0.13791 s
结果不太对 ,按理说,
多进程并行的耗时应该是
同步串行的三分之一,毕竟三个同等体量的任务在同时进行 。
多线程并发比
同步串行慢是应该的 ,因为
多线程并发和
同步串行的算力是一样的 ,但是多线程并发得在各个任务间来回切换,导致更慢 。
你问
@timer是什么意思?哦,这个是我写的修饰器 ,如下。
def timer(func): @wraps(func) def inner_func(): t = time.time() rts = func() print(f"timer: using {time.time() - t :.5f} s") return rts return inner_func
不太明白『Python修饰器』的老铁,不如给我点个「在看」,再关注下我 ,咱们以后详细道来。
我是小拍,微信 PiperLHJ ,感谢关注与在看 。