如何让 Python 处理速度翻倍?内含代码

概念篇在理解协程这个概念及其作用场景前,先要了解几个基本的关于操作系统的概念,主要是进程、线程、同步、异步、阻塞、非阻塞,了解这几个概念,不仅是对协程这个场景,诸如消息队列、缓存等,都有一定的帮助 。接下来,编者就自己的理解和网上查询的材料,做一个总结 。
进程在面试的时候,我们都会记住一个概念,进程是系统资源分配的最小单位 。是的,系统由一个个程序,也就是进程组成的,一般情况下,分为文本区域、数据区域和堆栈区域 。
文本区域存储处理器执行的代码 (机器码),通常来说,这是一个只读区域,防止运行的程序被意外修改 。
数据区域存储所有的变量和动态分配的内存,又细分为初始化的数据区 (所有初始化的全局、静态、常量,以及外部变量) 和为初始化的数据区 (初始化为 0 的全局变量和静态变量),初始化的变量最初保存在文本区,程序启动后被拷贝到初始化的数据区 。
堆栈区域存储着活动过程调用的指令和本地变量,在地址空间里,栈区紧连着堆区,他们的增长方向相反,内存是线性的,所以我们代码放在低地址的地方,由低向高增长,栈区大小不可预测,随开随用,因此放在高地址的地方,由高向低增长 。当堆和栈指针重合的时候,意味着内存耗尽,造成内存溢出 。
进程的创建和销毁都是相对于系统资源,非常消耗资源,是一种比较昂贵的操作 。进程为了自身能得到运行,必须要抢占式的争夺 CPU 。对于单核 CPU 来说,在同一时间只能执行一个进程的代码,所以在单核 CPU 上实现多进程,是通过 CPU 快速的切换不同进程,看上去就像是多个进程在同时进行 。
由于进程间是隔离的,各自拥有自己的内存内存资源,相比于线程的共同共享内存来说,相对安全,不同进程之间的数据只能通过 IPC(Inter-Process Communication) 进行通信共享 。
线程线程是 CPU 调度的最小单位 。如果进程是一个容器,线程就是运行在容器里面的程序,线程是属于进程的,同个进程的多个线程共享进程的内存地址空间 。
线程间的通信可以直接通过全局变量进行通信,所以相对来说,线程间通信是不太安全的,因此引入了各种锁的场景,不在这里阐述 。
当一个线程崩溃了, 会导致整个进程也崩溃了, 即其他线程也挂了, 但多进程而不会, 一个进程挂了,另一个进程依然照样运行 。
在多核操作系统中,默认进程内只有一个线程,所以对多进程的处理就像是一个进程一个核心 。
同步和异步【如何让 Python 处理速度翻倍?内含代码】同步和异步关注的是消息通信机制,所谓同步,就是在发出一个函数调用时,在没有得到结果之前,该调用不会返回 。一旦调用返回,就立即得到执行的返回值,即调用者主动等待调用结果 。所谓异步,就是在请求发出去后,这个调用就立即返回,没有返回结果,通过回调等方式告知该调用的实际结果 。
同步的请求,需要主动读写数据,并且等待结果;异步的请求,调用者不会立刻得到结果 。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用 。
阻塞和非阻塞阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态 。
阻塞调用是指调用结果返回之前,当前线程会被挂起 。调用线程只有在得到结果之后才会返回 。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程 。所以,区分的条件在于,进程 / 线程要访问的数据是否就绪,进程 / 线程是否需要等待 。
非阻塞一般通过多路复用实现,多路复用有 select、poll、epoll 几种实现方式 。
协程在了解前面的几个概念后,我们再来看协程的概念 。
协程是属于线程的,又称微线程,纤程,英文名 Coroutine 。举个例子,在执行函数 A 时,我希望随时中断去执行函数 B,然后中断 B 的执行,切换回来执行 A 。这就是协程的作用,由调用者自由切换 。这个切换过程并不是等同于函数调用,因为它没有调用语句 。执行方式与多线程类似,但是协程只有一个线程执行 。
协程的优点是执行效率非常高,因为协程的切换由程序自身控制,不需要切换线程,即没有切换线程的开销 。同时,由于只有一个线程,不存在冲突问题,不需要依赖锁 (加锁与释放锁存在很多资源消耗) 。
协程主要的使用场景在于处理 IO 密集型程序,解决效率问题,不适用于 CPU 密集型程序的处理 。然而实际场景中这两种场景非常多,如果要充分发挥 CPU 利用率,可以结合多进程 + 协程的方式 。后续我们会讲到结合点 。


推荐阅读