Python并发编程

1.操作系统层面的知识

要谈Python并发编程,我们就必须先谈操作系统相关的一些基础知识。

1.1 进程与线程

1.2 多线程与多核

三种线程:内核线程、轻量级进程、用户线程

内核线程(Kernel Thread, KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换。内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。一般一个处理核心对应一个内核线程,比如双核处理器对应两个内核线程等。
顺便提一下,现在的电脑一般是双核四线程、四核八线程,是采用超线程技术将一个物理处理核心模拟成两个逻辑处理核心,对应两个内核线程,所以在操作系统中看到的CPU数量是实际物理CPU数量的两倍,假如你的电脑是双核四线程,打开“任务管理器\性能”可以看到4个CPU的监视器。

程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP)。轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度。

用户线程与内核线程的对应关系有三种模型:一对一模型、多对一模型、多对多模型。在现在流行的操作系统中,大都采用多对多的模型。

多核并行处理 python python多核多线程编程_多线程

1.3 传统的并发策略、任务分类

并发策略:多进程、多线程
任务分类:CPU密集型、IO密集型。换句话说就是偏重于计算还是侧重输入输出。

1.4 多进程与多线程的对比

多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。多进程模式的缺点是创建进程的代价大。
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。

1.5 简单提一下:异步IO

考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。
对应到Python语言,单线程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。

2.Python中的并发编程

2.1 GIL(Global Interpreter Lock)全局解释器锁

2.1.1 线程安全

线程安全可以保证在多个线程同时执行的情况下程序可以正确执行。

为了做到线程安全,不同的编程语言采取不同的加锁方式。大体有两个级别的:
细粒度的锁,仅在需要的时候才加,因为编程语言不知道你什么时候需要加锁,所以这个任务就要给编程人员去掌控,java就是采取这种策略,jython(Python 的java实现)也是采用的这个方式;
粗粒度的锁,为了绝对的保证线程的安全,采取全局加锁的策略,这个由编程语言直接管理,编程人员不用操心,典型的是CPython(我们说的Python默认就是CPython)。
简单的说前一种更安全,后一种更方便。

2.1.2 GIL

虽然Python解释器中可以“运行”多个线程,保证在任意时刻,只有一个线程在解释器中运行。这种方式避免了多个线程的并发执行,因而保证了线程的安全。但是线程却无法并发执行了。

多线程在Python中只能交替执行,即使在多核环境下,也只能用到1个核。在Python中,可以使用多线程,但不要指望能有效利用多核。只能通过多进程实现多核任务。

GIL导致的结果
(1)Python中的多线程编程无法实现真正的多线程并发执行,而是多个线程的交替执行
(2)多线程编程无法有效利用多核

2.2 Python中的多线程编程方法

如上所述,由于Python中的GIL,多线程编程不是真正的多线程并发执行,无法有效利用多核。那在Python中是不是就没有必要进行多线程编程了呢?
结论是,有必要。

A:多线程最开始就不是用来解决多核利用率问题的。
是用来解决IO占用时CPU闲置问题的。
A:有必要,至少能解决很多IO阻塞问题
当发生阻塞时,Python是不耗CPU的,此时如果就一个线程就没法处理其他事情了。所以对于含有IO阻塞的环境。多线程至少有机会让你把一个CPU核心跑到100%。
另一个用处来自于Python的C扩展模块在扩展模块里是可以释放GIL的。但释放GIL期间不应该调用任何Python API。所以,对于一些非常繁重的计算,可以写成C模块(C模块内是可以真正多线程的),计算前释放GIL,计算后重新申请GIL,并将结果返回给Python。这样就可以让Python这个进程利用更多的CPU资源。

2.3 Python中的多进程编程方法

2.4 Python2中并发方式的选择

对于CPU密集型,效率关系:多进程 > 单进程(单线程)> 多线程
对于I/O密集型,效率关系:多线程 > 多进程 > 单进程(单线程)

并发方式选择:CPU密集型的任务,用多进程;I/O密集型的任务,用多线程。

2.5 异步IO模型

异步IO:当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。
异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程。

消息模型是如何完成异步IO的呢?当遇到IO操作时,代码只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程。当IO操作完成后,将收到一条“IO完成”的消息,处理该消息时就可以直接获取IO操作结果。

概括

在“发出IO请求”到收到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。

2.6 协程(Coroutine)

协程,又称微线程、协同程序。协程看上去也是子程序(函数),但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
其实就是异步编程概念的体现,是python支持异步编程的改进。与传统的异步编程的回调(callback)方式不同,协程看上去是用同步的方式在写异步代码
协程适用于解决IO密集型任务。(在Python中CPU密集型任务,只能通过多进程才能解决,多线程和协程只能解决IO密集型任务)

2.6.1 协程相比多线程的优势

(1)最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销
(2)第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

协程虽然相较于python的多线程而言效率更高,相比python的“表面多线程编程”,协程的确完成了多任务的并发执行。但协程仍然是单线程执行,多核的效用还是没有发挥。

因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

2.6.2 Python中协程的发展历程

大概经历了如下三个阶段:

  1. 由最初的生成器变形yield/send
  2. 引入@asyncio.coroutine和yield from
  3. 在最近的Python3.5版本中引入async/await关键字

2.6.3 协程的代码示例

async def hello():
"Hello world!")
await asyncio.sleep(1)
"Hello again!")

注意,await可接收的对象必须是awaitable的对象。协程本身也被认为是awaitable对象。
awaitable对象:必须是定义了__await__()方法且这一方法必须返回一个不是协程的迭代器。

再看几个例子:

class Wait(object):  
"""
    由于 Coroutine 协议规定 await 后只能跟 awaitable 对象,
    而 awaitable 对象必须是实现了 __await__ 方法且返回迭代器
    或者也是一个协程对象,
    因此这里临时实现一个 awaitable 对象。
    """
def __init__(self, index):
        self.index = index
def __await__(self):
return (yield self.index)
 
async def jump_range(upper):  
0
while index < upper:
await Wait(index)
if jump is None:
1
        index += jump
jump = jump_range(5)  
print(jump)  
print(jump.send(None))  
print(jump.send(3))  
print(jump.send(None)) 
 
<coroutine object jump_range at 0x10e2837d8>
0
3
4

 

2.6.4 gevent

gevent是一个基于libev和协程的python并发框架,以微线程greenlet为核心,使用了epoll事件监听机制以及诸多其他优化而变得高效.而且其中有个monkey类, 将现有基于Python线程直接转化为greenlet(类似于打patch)。
来个例子:

场景:有100个url,要对这100个url并发发起请求,通过个url丢进去,然后执行完成后,100个url请求的结果独立返回给我,如果是true那么就是返回请求的结果,如果是false,那么返回错误的原因。

#!/usr/bin/env python
#coding=utf-8
 
import gevent
import gevent.monkey
gevent.monkey.patch_socket() #重要
 
import urllib2
import time
 
def fetch(pid, url):
'Process %s: %s start work' % (pid, url))
None
None
None
try :
        req_obj = urllib2.Request(url)
10)
        status = response.code
        result = response.read()
True
except Exception, e :
        error_msg = str(e)
False
 
'Process %s: %s %s' % (pid, url, status))
return (pid, flag, url, status, error_msg)
 
def asynchronous():
0
    jobs = []
'http://107.167.184.223/'
for i in range(0, 100) : 
        jobs.append(gevent.spawn(fetch, i, url))
    gevent.joinall(jobs)
    value = []
for job in jobs:
#      print job.value
        value.append(job.value)
 
start = time.time()
print('Asynchronous:')
asynchronous()
stop = time.time()
print stop-start
 
#结果:比同步性能提升10倍
Process 0: http://107.167.184.223/ start work
Process 1: http://107.167.184.223/ start work
...
Process 99: http://107.167.184.223/ start work
Process 62: http://107.167.184.223/ 200
Process 40: http://107.167.184.223/ 200
...
Process 33: http://107.167.184.223/ 200
Process 59: http://107.167.184.223/ 200
Process 17: http://107.167.184.223/ 200
13.1921060085

注意,这里的monkey patch,这个patch针对python几个阻塞的库都做了patch(socket,DNS,select,thread,httplib等),打了money patch之后,这些库都会变成非阻塞,然后统一由gevent的主coroutine调度。只需要在最开始打个patch就行。

gevent是可以作为WSGI Server的。而且性能几乎超过uWSGI。自然比Tornado也要高。