Python 进阶:百万「并发」基础之异步编程(上篇)

HackPython 致力于有趣有价值的编程教学

简介

因为 GIL (全局锁) 的存在,Python 在运行性能方面一直是个短板,但在 IO 密集型网络编程里,利用 aysncio 等异步处理的方式可以提升百倍运行效率,但在计算密集型程序中,Python 并不是最佳的选择。

异步编程会涉及比较多的概念,为了减轻阅读压力,将其分为上、中、下三篇文章。

关键概念

要理解异步编程,首先需要理解阻塞、非阻塞、同步、异步以及并发、并行等概念与关系。

阻塞与非阻塞

当程序在等待某个操作完成期间,自己无法做其他事情的状态称为「阻塞」,专业点讲,当程序因未得到运行所需要的计算资源而被挂起的等待状态称为「阻塞」,「阻塞」其实无处不在,常见的有:磁盘 IO 阻塞、网络 IO 阻塞。

与「阻塞」对应的就是「非阻塞」状态,所谓「非阻塞」指的是程序在等待某个操作时,自己还可以做其他事情,需要注意的是,「非阻塞」并不可以在任何程序级别都存在,只有当程序可以囊括独立子程序单元时,它才可能存在非阻塞状态,其本质原因在于 CPU 单个核在那个瞬间只能处理一件事情。

同步与异步

与「阻塞」和「非阻塞」关联很大的概念分别是「同步」与「异步」。「同步」指不同的程序单元为了完成某个任务通过某种通信方式协调一致,此时可以称这些程序是同步执行的,这里协调一致并不是说要同时做,而是指大家有个顺序,「同步其实也就意味着有序」。

对应的「异步」指不同程序单元直接不需要协调也可单独完成任务,通常,没有先后顺序关联的业务逻辑可利用异步的方式来实现,比如爬虫下载不同的网页、保存等操作都可以独立完成,下载程序单元直接无需通信协调,这也造成了「异步」是无序的。

一个「非阻塞」进程中通常由多个独立的子进程构成,如执行到下载逻辑时,交由下载子进程去执行下载逻辑,而自身继续之前的其他逻辑,这个「交由下载子进程」的动作就可以称为「异步」。

关于「同步」或「异步」描述中提出通信协调通常指异步编程或「并发」编程的同步原语,如信号量、锁、同步队列等。

从具体的技术层面而言,如果调用一个耗时函数,函数会挂起直到执行完毕,返回结果,那么这个函数所在程序就是「阻塞」的,其操作就是「同步」的,如果调用一个耗时函数立刻返回,等需要的数据到达后再通知函数的调用者,则该函数所在程序就是「非阻塞」的,其操作就是「异步」的。

并发与并行

「同步」和「异步」的概念可能会与「并发」和「并行」混淆,但两者其实描述的是不同级别的事情,对于「并发」和「并行」,Erlang 之父 Joe Armstrong 给出的图很好的解释了两者的区别

「并发」是两个队列交替使用一台咖啡机,「并行」是两个队列同时使用两台咖啡机。

「并发」表示多个程序可以在同一个时间段内被执行,主要用于描述程序的组织结构,我们称这个程序是可以并发的通常指该程序设计了多个可独立执行的子任务,从而可以在同一时间段内利用有限的计算资源让多个任务以近实时的形式执行。

而「并行」表示多个程序可以在同一时刻被执行,「并行」的关键要有物理上的支持,比如有两台咖啡机,它通常用于描述程序的执行状态而不是程序组织结构,通常用来表示有足够的计算资源让多个任务同时执行。

更严谨的描述为:1.「并发」是说进程 B 的开始时间是在进程 A 的开始时间与结束时间之间,我们就说 A 和 B 是「并发」的。2.「并行」是「并发」的真子集,指同一时间两个进程运行在不同的机器上或者同一个机器不同的核心上。

简单总结上面的概念:非阻塞可以提高程序整体的执行效率,异步是一种组织非阻塞任务的方式 (即操作其中的程序单元),而并发是为了让独立的子任务有机会被尽快执行,但不一定可以加快任务整体的进度,而并行则是利用多核资源加快多任务的完成进度。

要实现并发,就需要将整体任务拆分为多个相互独立的子任务,而不同子任务之间才会有所谓的阻塞 / 非阻塞、同步 / 异步等说法。并发、非阻塞、异步三个概念总是如影随形。

进程、线程与协程

「进程」是操作系统中的最小的资源分配单位,每个「进程」都有各自独立的地址空间、资源句柄,它们相互独立,每个「进程」中都会有个用于描述当前进程的数据结构,操作系统会利用这些描述来管理进程,同时对操作系统而言,进程的创建与销毁是比较消耗资源的。

「进程」会抢占式的争夺 CPU 资源,同一时刻下,CPU 的一个核只能执行一个进程,而单核 CPU 可以快速切换不同的进程来使得多进程顺序执行看上去像在同时执行。

「线程」是 CPU 调度的最小单位,「线程」是进程的一个实体,一个进程可以包含多个「线程」,同一进程的多个「线程」是可以共享当前进程的内存地址空间与资源句柄的,「线程」的切换需要操作系统来实现调度,我们写的程序无法控制这个调度过程。线程适用于 IO 密集型任务。

如果想人为的控制线程的调度过程,可以使用「协程」,「协程」不等于可以自己调度的线程,它是属于线程的,即一个线程可以有多个「协程」,算是一种用户状态下的轻量级线程,协程的调度可以完全由用户做主,因此使用更加灵活,可以将协程理解为可以在特定位置暂停或恢复的函数 / 子程序。

异步编程

异步编程就是以进程、线程、协程、函数作为执行任务程序的基本单位,结合回调、事件循环、信号量等机制,以提高程序整体执行效率和并发能力的编程方式。

无论什么语言,要想实现异步编程就跳不开回调与事件循环。

在执行耗时函数时,函数会提前注册回调函数返回给调度程序,而函数耗时操作的等待与监听任务交给了操作系统,当操作系统监听到耗时操作的状态改变后就调用回调函数通知调度程序,这整个过程称为「回调」。

而「事件循环」是一种等待程序分配事件或消息的编程框架,简单来说就是当「 A 发生时,执行 B」,即监听当有什么发生时,就去执行什么,事件循环本质就是一种循环,它会不停的收集事件并执行对应这些事件的响应逻辑。

因为异步编程离不开「回调」与「事件循环」,但「回调」与「事件循环」的使用容易出现回调地狱、堆栈撕裂、错误定位与处理困难等问题,Python 基于 asyncio 标准库来实现事件循环,为了避免其带来的危害,衍生出了基于协程的处理方案,协程本身的特性也满足了回调的要求。

在 Python 中协程与 asyncio 一同构建了其异步编程的整体框架。

结尾

在本节中,主要介绍了异步编程中会涉及的各种概念,在下一篇中,会接着探讨 Python 中的异步编程,最后欢迎学习 HackPython 的教学课程并感谢您的阅读与支持。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懒编程-二两

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值