0%

Python 并发编程札记 (Part 1)

这是 Python 并发编程学习笔记的开篇,将会从并发编程涉及的概念以及实现角度来归纳之前的学习成果,包括常见的进程线程协程问题以及现存的活跃度高的 Python 异步编程框架,可以说是为之后高并发的实现做的铺垫。这一篇主要是阐述了三组不同但经常会被混淆的概念:并发/并行、同步/异步、阻塞/非阻塞。

并行与并发

以下的说明都以 socket 服务器为例,来对比并发和并行的概念。

  • 并行 Parallelism : 指两个或者多个事件在同一时刻发生。以 可独立执行的进程集合 的方式编程。
  • 并发 Concurrency : 指两个或多个事件在同一时间间隔内发生。以 可同时执行的(可能相关的)计算指令 的方式编程。

并发

比方说,服务器能建立 1000 个 TCP 连接,即服务器同时维持了 1000 个 socket,那么并发量就是 1000,但是服务器计算机只有单核或者 8 核,16 核等,实际上 1000 个 socket 连接也是要分时处理的。

并发在小粒度层面上还不算严格意义上的同时。

并行

而并行就是实际上同时在运行的任务,同样拿上面这个 socket 服务器来说,假如服务器的编程语言(不是像 Python 的 GIL 是单线程分时那样的”多线程”)充分利用了服务器计算机的多核心性能,那么并行数就是 8(8核)或者 16(16核)。

由此可见,并发更多的是逻辑意义(可以特指为代码层面)上的同时,有并行的潜力(假如服务器计算机就有 1000 个核心也说不定啊哈哈),而并行更多的关注到物理运行状态。

相关引用

  1. 根据 CSAPP(《深入理解计算机系统》)的描述,并行是并发的真子集,即存在 [并发程序[并行程序, 串行程序], 顺序程序] 的包含关系。
  2. 根据《七周七语言》的描述, 并发是指同时有很多事要做,你可以串行处理也可以并行处理。并行是指同时做多件事。因此并发和并行是相关的,但是是不同的两个概念。

举例说明

假如你在上课过程中(程序运行),一边听老师讲课,一边在课桌下偷偷看小说,在宏观层面上 定义 你是在同时干两件事(并发),但是大脑( CPU 资源)一般人是有限的,这里类比为单核,那么实际上没有同时执行(即没有既听到课又看到书)。虽然是同时在干两件事,实际上执行的大概就只是看小说吧! :)


同步/异步 与 阻塞/非阻塞

(同步/异步) 以及 (阻塞/非阻塞) 的概念经常会混为一谈(尽管他们是两组不同的概念),用于描述任务之间(以消息通知方式)协作的处理模式。而上面所提到的并发,定义为多个事件同时发生,如果我们把这些同时发生的事件理解为是我们自己执行的多个任务(在计算机层面上来说确实如此),就会存在多任务协作的可能。

举个例子

对于“利用消息通知来相互协作来完成任务”,你会想到什么场景呢?这里我举一个不常见的例子:老板让员工 A 去银行办理业务 C(要完成的目标任务),A 去和银行业务人员 B 洽谈交接(以消息通知方式,在这里 A 就是消息的发起方,B 就是消息的接收方/响应方),A 告诉了 B 他的来意,B 说“目前人太多你,你先拿号排队吧”,于是乎 A 就取号然后在现场玩起了手机,不时看看到自己了没有(同步轮询),好一会终于轮到 A 了(以上过程表现为:同步非阻塞),B 告诉 A 需要现场提交资料。A 提交了一些必要的资料给到 B,B 先检查资料是否齐全,A 就在现场静静地等待 B 的检查结果(以上过程表现为:同步阻塞),片刻之后 B 告诉 A,“你的资料齐全,之后我会到提交监管部门审核,大概 3 天之后会得到监管部门的结果再通知你”,A 说“好的,到时你打这个电话通知我”(设置回调),之后 A 离开银行不处理和 C 相关的工作。3 天之后 B 打电话(回调调用)告诉 A 审核通过,A 继续处理业务 C 相关的工作(以上过程表现为:异步)直到完成,最后报告老板。

这个例子几乎包含了我们下面要介绍的所有内容,而且我们可以轻易的发现,这里表现为 (同步/异步) 以及 (阻塞/非阻塞) 的行为几乎都是发生在消息通信的过程中。

让我们抽象一点表述

继续从消息通信这个点切入,(同步/异步)是从 获得消息响应的机制 角度来考虑的,而(阻塞/非阻塞)是从 等待消息消息时的状态 来考虑的。

无论是同步还是异步,阻塞还是非阻塞,角度的出发点都是对于消息发起人而言。

同步意味着当前只关注一个任务,比如你一直需要专注于交流这件事情,对方不回答、不响应之前我不能继续我的下一个任务;

而异步就是我(消息发起方)知道我会有人告诉我事情的结果,响应我的请求,但我不会傻等而是继续下一个任务,我告诉他如果搞定要如何提醒我(设置回调,或者让其返回一个 Task “任务”),之后就去干别的事情,也就是说不会等待,但是我可以根据需要随时问他结果(调用 Task),或者他搞定之后通知我(调用回调)。那么这就意味着,异步是没有(阻塞/非阻塞)之分的。

同步

同步在编程世界中非常常见,因为它符合我们的常规思维,是一种保证序列顺序可靠的做法,而编程大多数情况下都是需要顺序完成的(如同计算机的构造,也是顺序执行指令为主)。

脱离消息通信仅从任务的角度来说,同步意味着多个任务之间协同步调,共同完成一个大任务。但是实际情况是,任务自身完成的条件和具体内容都是不同的,所以任务完成有先有后。如果一个任务需要另外一个或多个任务完成它才能继续完成,那么这个大任务就可以说是采用了同步处理的方式。

结合消息通信来说,同步表现为消息发起方需要一直等待响应方响应,才能继续下一个通信执行。

异步

异步的概念是相对于同步来说的,他们的不同点在于作为调用方/发起方,我到底等不等对方响应

即便也是在多个任务共同完成一个大任务的背景下,也同样是存在任务之间的相互依赖,异步处理就非常“任性”:即使我依赖你的完成结果,但是我也只是告诉你你要完成什么目标,到时候你通过什么渠道把完成结果告诉我(异步其实并没有要求这一个动作,只不过这是一个可靠性的保证,保证依赖的任务一定会完成),我继续做我接下来要做的事情。

结合消息通信来说,异步表现为消息发起方/调用方发起消息之后,不会等待响应方响应而继续执行,把这个担子丢给响应方,让它自己在实际处理完成后通过状态、通知或者回调来告诉发起方。

Node.js 就是一个常见的异步单线程的编程模型。

阻塞

阻塞/非阻塞可以理解为要不要“傻等”,因为在异步处理中是直接继续下一个任务或是任务的下一步操作,因此就没有所谓的等待。在同步中过程中,如果依赖的任务没有完成,在等待期间我要干什么,或者是说我表现出怎样的态度(状态)就是阻塞和非阻塞的分别。

那么从消息通信的角度来说,消息通信的主体必定具有最少两个状态,等待和非等待。具体到如 IPC 上来说,进程/线程的状态来说,涉及到必定有 就绪、运行以及等待/阻塞 这三个状态。在同步阻塞中,执行任务中的线程/进程会进入到等待/阻塞状态,不释放 CPU 资源,同时也不干其他事情,直到得到进程/线程调用结果(消息响应)才会继续下一步操作。

非阻塞

顾名思义,非阻塞作为与阻塞相对的概念,意味着它是不进入到所谓的等待状态,而是继续执行。具体到如 IPC 而言,线程/进程不进入到 blocking 状态,释放 CPU 资源,执行该如何处理就继续如何处理,但是会有另外机制去轮询看是否得到返回结果。

但是非阻塞和异步之间的区别在于,异步是把得到响应的担子丢给了响应方,由响应方自己根据发起方提供的手段返回响应,而非阻塞则需要有单独的轮询机制,由发起方不时的试探响应到底产生了没有。


小结

在介绍并发编程中,把这几个经常会出现的抽线概念先解释了一遍,目的在于在以后的篇幅中,可以降低理解的门槛或者说统一一下口径。因为我们之后会发现,所有的这些并发编程技巧亦或者是异步框架,他们的设计思想几乎都离不开这些概念。下面我们会深入到进程、线程以及协程的角度去理解并发编程。