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")
])

 

进程、线程、协程的区别和选择?

进程是资源分配的单位,真正执行代码的是线程,操作系统真正调度的是线程。

进程没有线程效率高,进程占用资源多,线程占用资源少,比线程更少的是协程。

协程依赖于线程、线程依赖于进程,进程一死线程必挂,线程一挂协程必死。

一般不用多进程,可以考虑使用多线程,如果多线程里面有很多网络请求,网络可能会有堵塞,此时用协程比较合适。

posted @ 2018-11-26 09:51  wangcoo  阅读(1033)  评论(0编辑  收藏  举报