Python-Core-50-Courses/第32课:Python中的并发编程(1).md

6.8 KiB
Raw Blame History

第032课Python中的并发编程1

现如今我们使用的计算机早已是多CPU或多核的计算机为此我们使用的操作系统基本都是支持“多任务”的操作系统这样的操作系统使得我们我们可以同时运行多个程序也可以将一个程序分解为若干个相对独立的子任务让多个子任务“齐头并进”的执行从而缩短程序的执行时间同时也让用户获得更好的体验。因此当下不管用什么编程语言进行开发实现让一个程序同时执行多个任务已经成为程序员的标配技能。为此我们需要先了解两个重要的概念多进程和多线程。

线程和进程

我们通过操作系统运行一个程序会创建出一个或多个进程进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。简单的说进程是操作系统分配存储空间的基本单位每个进程都有自己的地址空间、数据栈以及其他用于跟踪进程执行的辅助数据操作系统管理所有进程的执行为它们合理的分配资源。一个进程可以通过fork或spawn的方式创建新的进程来执行其他的任务不过新的进程也有自己独立的内存空间因此两个进程如果要共享数据必须通过进程间通信机制IPC来实现具体的方式包括管道、信号、套接字等。

一个进程还可以拥有多个并发的执行线索简单的说就是拥有多个可以获得CPU调度的执行单元这就是所谓的线程。由于线程在同一个进程下它们可以共享相同的上下文因此相对于进程而言线程间的信息共享和通信更加容易。当然在单核CPU系统中多个线程不可能同时执行因为在某个时刻只有一个线程能够获得CPU多个线程通过共享了CPU执行时间的方式来达到并发的效果。在程序中使用多线程技术通常都会带来不言而喻的好处最主要的体现在提升程序的性能和改善用户体验今天我们使用的软件几乎都用到了多线程技术这一点可以利用系统自带的进程监控工具如macOS中的“活动监视器”、Windows中的“任务管理器”来证实如下图所示。

这里,我们还需要跟大家说说另外两个概念:并发concurrency并行parallel。并发是指同一时刻只能有一条指令执行但是多个线程对应的指令被快速轮换地执行。比如一个处理器它先执行线程 A 的指令一段时间,再执行线程 B 的指令一段时间,再切回到线程 A 执行一段时间。由于处理器执行指令的速度和切换的速度非常非常快人完全感知不到计算机在这个过程中有多个线程切换上下文执行的操作这就使得宏观上看起来多个线程在同时运行但微观上其实只有一个线程在执行。并行是指同一时刻有多条指令在多个处理器上同时执行并行必须要依赖于多个处理器不论是从宏观上还是微观上多个线程都是在同一时刻一起执行的。在我们的课程中其实并不用严格区分并发和并行两个词所以我们把Python中的多线程、多进程以及异步I/O都视为实现并发编程的手段但实际上前面两者也可以实现并行编程当然这里还有一个全局解释器锁GIL的问题我们稍后讨论。

多线程编程

Python标准库中threading模块的Thread类可以帮助我们非常轻松的实现多线程编程。我们用一个联网下载文件的例子来对比使用多线程和不使用多线程到底有什么区别,代码如下所示。

不使用多线程的下载。

import random
import time


def download(*, filename):
    start = time.time()
    print(f'开始下载{filename}.')
    time.sleep(random.randint(3, 6))
    print(f'{filename}下载完成.')
    end = time.time()
    print(f'下载耗时: {end - start:.3f}秒.')


start = time.time()
download(filename='Python从入门到住院.pdf')
download(filename='MySQL从删库到跑路.avi')
download(filename='Linux从精通到放弃.mp3')
end = time.time()
print(f'总耗时: {end - start:.3f}秒.')

说明:上面的代码并没有真正实现联网下载的功能,而是通过time.sleep()休眠指定的时间模拟下载文件需要花费一段时间。

运行上面的代码,可以得到如下所示的运行结果。可以看出,当我们的程序只有一个工作线程时,每个下载任务都需要等待上一个下载任务执行结束才能开始,所以程序执行的总耗时是三个下载任务各自执行时间的总和。

开始下载Python从入门到住院.pdf.
Python从入门到住院.pdf下载完成.
下载耗时: 3.005秒.
开始下载MySQL从删库到跑路.avi.
MySQL从删库到跑路.avi下载完成.
下载耗时: 5.006秒.
开始下载Linux从精通到放弃.mp3.
Linux从精通到放弃.mp3下载完成.
下载耗时: 6.007秒.
总耗时: 14.018秒.

事实上,上面的三个下载任务之间并没有逻辑上的因果关系,三者是可以并发的,没有必要等待上一个下载任务结束,为此,我们可以使用多线程编程来改写上面的代码。


再次运行程序,我们可以发现,整个程序的执行时间几乎等于耗时最长的一个下载任务的执行时间,这也就意味着,三个下载任务是并发执行的,没有一个等一个,这样做很显然提高了程序的执行效率。

通过上面的代码可以看出,直接使用Thread类的构造器就可以创建线程对象,而线程对象的start()方法可以启动一个线程。线程启动后会执行target参数指定的函数当然前提是获得CPU的调度如果target指定的线程要执行的目标函数有参数,需要通过args参数为其进行指定。

除了上面的代码展示的创建线程的方式外,还可以通过继承Thread类并重写run()方法的方式来自定义线程,具体的代码如下所示。


通过上面的例子可以看出如果程序中有非常耗时的执行单元而这些耗时的执行单元之间又没有逻辑上的因果关系即B单元的执行不依赖于A单元的执行结果那么A和B两个单元就可以放到两个不同的线程中让他们并发的执行。这样做的好处除了减少程序执行的等待时间还可以带来更好的用户体验因为一个单元的阻塞不会造成程序的“假死”因为程序中还有其他的单元是可以运转的。

温馨提示:学习中如果遇到困难,可以加QQ交流群询问。

付费群:789050736,群一直保留,供大家学习交流讨论问题。

免费群:151669801,仅供入门新手提问,定期清理群成员。