Python并发编程
一、前言
1.为什么要搞并发编程
在大学阶段,科班出身的应该都学过《操作系统》这门课程,里面花了大篇幅的时间 去介绍进程、线程、并发、并行等概念。那么,并发编程需要各位的操作系统基础,本文不会对操作系统的部分进行过多的解释。
在这里,我们只简单的告诉大家为什么要搞并发编程。举一个例子:有一大堆砖需要搬,一个人干肯定要花很多时间(因此,如果你只会if……elif,那么很多时候,真的会拖慢运行速度)。多线程就好比是增加了人手,所以干的快。简而言之:就是为了增加运行速度
而随着编程的不断深入,为了使运行速度更快,并发编程几乎是任何程序员都绕不过去的东西。
2.增加运行速度的方法
在现实当中,我们有很多办法来提升程序的运行速度,比如说:我可以对某个算法进行优化,但是,这种优化往往比较有限。因此,我们可以采用多线程,多CPU,以及多机器并行等方式。
其中,多机器并行当中,就包括我们常常听说的大数据之类的东西,包括Spark,Hadoop等。而大数据是一个单独的学科,非常值得研究,因此在这里,我们只介绍一下跟并发编程相关的东西。
3.实现Python并发的方式
主要有三个:多线程,多进程,多协程。学过《操作系统》的同学,应该对这三个概念都不陌生。如果要了解这三个概念之间到底是什么关系,我们首先得了解一下CPU密集型,以及I/O密集型
3.1程序分类
3.1.1 CPU密集型(CPU-bound)
其实通过英文翻译,不难发现,英文和中文翻译还是有点意思上的差异的。所谓的CPU-bound,其实就是说:运行的速度最终会受到CPU计算的限制。因此CPU-bound有些时候也被翻译成计算密集型。具体就是指:I/O相对很少,但是却需要CPU进行非常大的计算处理,因此,CPU占用率非常的高。比如说:压缩、解压缩;加密/解密;正则表达式搜索等等。
3.1.2 I/O密集型(I/O bound)
这个是与CPU-bound相对的概念。与CPU-bound相反,此类程序计算相对较少,但是要花费大量的时间在I/O上面,CPU占用率低,但是运行速度却怎么也提不上去。这个时候就要考虑异步I/O(异步IO其实属于多协程)。那么此类程序包括:文件处理程序、爬虫程序、数据库的读取程序等。
3.2 多线程,多进行,多协程的对比
在《操作系统》这门课程当中,科班的同学大体都比较熟悉这几段话:
- 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是结构的基础。
- 线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元
- 协程,英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程
如果用一幅图,来表示这三者的关系,那么就是:
但是,除了上面这些东西,还应当了解:
-
多进程:对应Python当中的multiprocessing。就是利用CPU的多核,来实现并行操作
- 优点:可以利用多核CPU并行运算
- 缺点:占用资源最多,可启动数目比线要少
- 适用于:CPU密集型计算
-
多线程:对应Python当中的threading。所谓的多线程,其实就是利用的CPU和I/O可以同时执行的原理,尽可能的别让CPU干等(学过操作系统的应该都明白这个道理)
- 优点:相比进程,线程更加轻量级,占用资源更少;
- 缺点:
- 相比进程,多线程只能并发执行,不能利用多CPU
- 相比协程,启动数目有限,更加占用内存资源,启动数目有限
- 适用于:I/O密集型计算,同时运行的任务数目不算很多
-
多协程:其中异步I/O就是属于这个模块的
-
优点:内存开销最少,启动数量最多
-
缺点:python当中对协程的支持比较少、代码实现复杂。
-
题外话:关于异步I/O
I/O其实也是可以异步执行的(对应的python当中的asynocio)。即:在一个线程当中,由于I/O部分并不完全需要CPU去执行,而CPU主要是完成计算功能,因此,在I/O的时候,能不能让CPU去完成相关函数的计算呢?
-
3.3如何选择
那么针对以上这些情况,Python都为我们提供了什么方法呢?
- 为了避免对资源的访问冲突,Python为我们提供了Lock等,对资源访问进行控制。
- 为了实现不同线程,不同进程之间的通信,Python提供了Queue等方法,我们可以用这个来实现生产者-消费者问题
- 为了简化线程或者进程的任务管理,Python为我们提供了线程池Pool
- 我们可以使用subprocess来启动外部程序的进程,实现并行输入输出的交互。
4.Python的全局解释器锁
为什么要介绍这个呢?因为它是Python运行比较慢的重要原因,当然了,除了底层封装的原因,Python本身边解释,边运行的特点,也注定Python确实比较慢。
全局解释器锁:(Global Interpreter Lock,简称:GIL)。由于并发,并行引发的进程或者线程的不同步,因此,需要一个机制让各个进程的运行保持同步。这就是GIL诞生的原因。但是,它比较简单粗暴,它使得任何时刻仅有一个线程在执行。即使是多核处理器,GIL也是如此
即:在计算的时候,开启GIL,I/O的时候,关闭GIL
Python之所以引入这么一个GIL,是历史遗留问题。由于Python最初就是用来做数据处理的,既然用来计算数据,我们并不希望由于进程或者线程不同步而引发数据计算出现结果不一致的问题,所以才引入了GIL机制,但是,随着Python的发展,Python应用的领域越来越多,网页编程,游戏开发等也会用Python,这个情况下,GIL反而成了累赘。
因此,在Python的并发编程当中,我们更多的还是要针对I/O,如果把多线程用于CPU密集型计算,由于GIL的存在,反而会拖慢速度。
与此同时,Python的开发者们也意识到了这个问题,于是就想到了一个办法:既然GIL只是针对线程的。那么,我用多进程不就可以了。所以Python才会出现multiprocessing
二、多进程编程
1.进程创建步骤
进程的创建大致分为如下几个步骤:
-
导入进程包:
import multiprocessing
-
通过进程类创建进程对象
pro = multiprocessing.Process() # pro为进程对象名
其中,关于这个Process,解释如下:
Process(group=None, target=None, name=None, args=(), kwargs={ }) """ 参数说明: 1 group——参数未使用,值始终为None 2 target——表示调用对象,即子进程要执行的任务 3 args——表示调用对象的位置参数元组 4 kwargs——表示调用对象的字典 5 name——为进程的名称 """
-
启动进程执行相关任务
pro.start()
2.多进程演示
import time
import multiprocessing
# sing
def sing():
for i in range(3):
print("sing……")
time.sleep(0.5)
# dance
def dance():
for i in range(3):
print("dance……")
time.sleep(0.5)
if __name__ == "__main__":
#创建进程
sing_process = multiprocessing.Process(target = sing)
dance_process = multiprocessing.Process(target = dance)
#启动进程
sing_process.start()
dance_process.start()
那么,Process当中的字典或者元组参数呢?
import time
import multiprocessing
# sing
def sing(num, name):
for i in range(num):
print("sing……")
time.sleep(0.5)
# dance
def dance(num,name):
for i in range(num):
print("dance……")
time.sleep(0.5)
if __name__ == "__main__":
#创建进程
sing_process = multiprocessing.Process(target = sing,args = (3,'xiaomi'))
dance_process = multiprocessing.Process(target = dance,kwargs = {
'name':'xiaohong','num':2})
#启动进程
sing_process.start()
dance_process.start()
那么上面这个程序就相当于,给sing,dance加了主语,并且还限定的循环次数。那么上面这个程序的运行结果,就是xiaomi sing……运行三次,xiaohong dance……运行两次。
注意:
- 如果要传入元组,那么元组的顺序要和参数的顺序保持一致,比如说:上面这个程序函数是sing(num,name)当中,那么你传入的元组第一个一定要是num,第二个才能是name,否则会出现一些异常
- 如果要传入字典:那么key和value对应上即可。
3.获取进程编号
在现实开发当中,往往可能并发程度很高。所以,进程数量就会很多。如果没有办法区分父进程,子进程,那么势必就会造成混乱。于是,进程当都要赋予他们编号(也就是《操作系统》当中经常提及的pID),方便管理。
获取进程主要有两种方法:
- os.getpid():获取当前进程的编号
- os.getppid():获取父进程的编号
import time
import multiprocessing
import os
# sing
def sing