python并发编程之多进程、多线程、异步、协程、通信队列Queue和池Pool的实现和应用
简单地说,就是操作系统可以同时运行多个任务。实现多任务有多种方式,线程、进程、协程。
并行和并发的区别?
并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的
真的多任务叫并行,假的多任务叫并发。
我们来了解下python中的进程,线程以及协程!
从计算机硬件角度:
计算机的核心是CPU,承担了所有的计算任务。 一个CPU,在一个时间切片里只能运行一个程序。
从操作系统的角度:
进程和线程,都是一种CPU的执行单元。
进程:表示一个程序的上下文执行活动(打开、执行、保存...)
线程:进程执行程序时候的最小调度单位(执行a,执行b...),可以简单理解为同一进程中有多个计数器,每个线程的执行时间不确定,而每个进程的时间片相等,线程是操作系统调度执行的最小单位
一个程序至少有一个进程,一个进程至少有一个线程。
多进程/多线程:
表示可以同时执行多个任务,进程和线程的调度是由操作系统自动完成。
进程:每个进程都有自己独立的内存空间,不同进程之间的内存空间不共享。 进程之间的通信有操作系统传递,导致通讯效率低,切换开销大。进程是系统进行资源分配的最小单位
线程:一个进程可以有多个线程,所有线程共享进程的内存空间,通讯效率高,切换开销小。线程不能够独立执行,必须依存在进程中。共享意味着竞争,导致数据不安全,为了保护内存空间的数据安全,引入"互斥锁"。
一个线程在访问内存空间的时候,其他线程不允许访问,必须等待之前的线程访问结束,才能使用这个内存空间。
互斥锁:一种安全有序的让多个线程访问内存空间的机制。
多线程:
密集I/O任务(网络I/O,磁盘I/O,数据库I/O)使用多线程合适。 threading.Thread、multiprocessing.dummy 缺陷:同一个时间切片只能运行一个线程,不能做到高并行,但是可以做到高并发。
但是,使用多线程,必须考虑到GIL 全局解释器锁:线程的执行权限,在Python的进程里只有一个GIL。
一个线程需要执行任务,必须获取GIL。
好处:直接杜绝了多个线程访问内存空间的安全问题。 坏处:Python的多线程不是真正多线程,不能充分利用多核CPU的资源。
但是,在I/O阻塞的时候,解释器会释放GIL。
什么是GIL?
GIL并不是Python的特性,Python完全可以不依赖于GIL。GIL全称Global Interpreter Lock。它是在实现Python解析器(CPython)时所引入的一个概念。GIL无疑就是一把全局排他锁。
Python GIL其实是功能和性能之间权衡后的产物,它尤其存在的合理性,也有较难改变的客观因素。因为GIL的存在,只有IO Bound场景下得多线程会得到较好的性能,如果对并行计算性能较高的程序可以考虑把核心部分也成C模块,或者索性用其他语言实现
GIL在较长一段时间内将会继续存在,但是会不断对其进行改进。
线程创建步骤:
import threading # 创建一个线程对象 t1 = threading.Thread(target=func_name, args=(num,), name=”子线程名字”) # 创建一个线程并启动 t1.start() # 等待子线程执行完毕之后再继续向下执行主线程 t1.join() 备注:主线程会等待子线程结束之后才会结束,主线程一死,子线程也会死。线程的调度是随机的,并没有先后顺序。
由于多线程之间共享全局变量就会导致出现资源竞争的问题,为了避免这种竞争出现,利用互斥锁可以实现线程同步。
实现方式:
# 创建锁 mutex = threading.Lock() # 加锁 mutex.acquire() # 释放锁 mutex.release()
多进程:
密集CPU任务,需要充分使用多核CPU资源(服务器,大量的并行计算)的时候,用多进程。 multiprocessing 缺陷:多个进程之间通信成本高,切换开销大。
进程创建步骤:
# 导入进程模块 import multiprocessing # 创建一个进程的实力对象 P = Multiprocessing.Process(target=func_name, [args=(元组), kwargs={字典}]) # 创建并启动进程 p.start() p.join(5)
通过Queue实现通信(注意进程和协程使用的队列不一样)
from queue import Queue # 线程、协程使用的队列 from multiprocessing import JoinableQueue as Queue # 进程使用的队列 q = Queue(maxsize=100) item = {} q.put_nowait(item) # 不等待直接放,队列满的时候会报错 q.put(item) # 放入数据,队列满的时候回等待 q.get_nowait() # 不等待直接取,队列空的时候会报错 q.get() # 取出数据,队列为空的时候会等待 q.qsize() # 获取队列中现存数据的个数 q.join() # 队列中维持了一个计数,计数不为0时候让主线程阻塞等待,队列计数为0的时候才会继续往后执行 q.task_done() # put的时候计数+1,get不会-1,get需要和task_done 一起使用才会-1
注意:(多进程队列的使用非常特殊)
多进程中使用普通的队列模块会发生阻塞,对应的需要使用multiprocessing
提供的JoinableQueue
模块,其使用过程和在线程中使用的queue方法相同
如果是通过进程池创建的进程,那么队列的使用要用
multiprocessing.Manager().Queue()的方式,否则会报错。
进程池的实现步骤
# 导入进程池模块 From multiprocessing import Pool # 定义进程池,最大进程池最大数 Po = Pool(3) # 通过进程池调用目标 apply_async非阻塞,不会等待子进程结束;apply阻塞,会等待子进程结束才结束 po.apply_async(func,(传递给目标的参数元祖,)) # 关闭进程池 Po.close() # 等待进程池执行完毕 Po.join()
注意:
线程如果要使用线程池:from multiprocessing.dummy import Pool
协程如果要使用协程池:from gevent.pool import Pool
协程:
又称微线程,在单线程上执行多个任务,用函数切换,开销极小。不通过操作系统调度,没有进程、线程的切换开销。genvent,monkey.patchall
多线程请求返回是无序的,那个线程有数据返回就处理那个线程,而协程返回的数据是有序的。
缺陷:单线程执行,处理密集CPU和本地磁盘IO的时候,性能较低。处理网络I/O性能还是比较高.
协程创建实例:
from gevent import monkey import gevent import random import time # 有耗时操作时需要 monkey.patch_all() # 将程序中用到的耗时操作的代码,换为gevent中自己实现的模块 def coroutine_work(coroutine_name): for i in range(10): print(coroutine_name, i) time.sleep(random.random()) gevent.joinall([ gevent.spawn(coroutine_work, "work1"), gevent.spawn(coroutine_work, "work2") ])
进程、线程、协程的区别和选择?
进程没有线程效率高,进程占用资源多,线程占用资源少,比线程更少的是协程。
协程依赖于线程、线程依赖于进程,进程一死线程必挂,线程一挂协程必死。