- 任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程。多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量。
- Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程在执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码上了锁。所以,多线程在Python中只能交替执行,并不能做到真正的并发执行。
- 所以在python中,通常使用协程来代替多线程。
协程
- 协程是一种用户级的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
- 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
- 在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其它协程共享全局数据和其它资源。
- 协程是用户自己来编写调度逻辑的,对CPU来说,协程其实是单线程,所以CPU不用去考虑怎么调度、切换上下文,这就省去了CPU的切换开销,所以协程在一定程度上又好于多线程。
协程基础
Python对协程的支持是通过generator实现的。generator也叫生成器。何谓生成器呢?就得先从另外一个叫迭代器的对象说起:
迭代器
- 迭代器是一个带状态的对象,调用next()方法的时候返回容器中的下一个值,任何实现了iter和next()方法的对象都是迭代器,iter返回迭代器自身,next返回容器中的下一个值,如果容器中没有更多元素了,则抛出StopIteration异常。
- 而生成器则是一种特殊的迭代器,不过这种迭代器更加优雅。它不需要再像上面的类一样写iter()和next()方法了,只需要一个yiled关键字。 生成器一定是迭代器(反之不成立),因此任何生成器也是以一种懒加载的模式生成值。
生成器
- 带有 yield 的函数在 Python 中被称之为 generator(生成器)。
def fib():
prev, curr = 0, 1
while True:
yield curr
prev, curr = curr, curr + prev
for n in fib(5):
print n
在 for 循环执行时,每次循环都会执行 fab 函数内部的代码
执行到 yield b 时,fab 函数就返回一个迭代值,下次迭代时
代码从 yield b 的下一条语句继续执行
而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield。
yield关键字
- yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,调用 fab(5) 不会执行 fab 函数,而是返回一个 iterable 对象
- send(msg)与next()的区别在于send可以传递参数给yield表达式,这时传递的参数会作为yield表达式的值,而yield的参数是返回给调用者的值。——换句话说,就是send可以强行修改上一个yield表达式值。比如函数中有一个yield赋值,a = yield
//这个代码如果不理解,可以使用pycharm打断点进行调试,就知道yield的运行流程了
import time
def consumer():
r = ''
while True:
n = yield r #n表示接受的值,r表示返回的值
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None) #启动生成器
//python 的 generator 初始化时还没有被运行。
//所以你直接send() 会报错,要首先调用__next__() 生成器才开始运行,
//send(None)==__next__()。开始运行之后才能正常 send()。
n = 0
while n < 5:
time.sleep(3)
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
c = consumer()
produce(c)
高级协程gevent
Python通过yield提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。
gevent它是一个并发网络库。它的协程是基于greenlet的,并基于libev实现快速事件循环
其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成
import gevent
def test1():
print 12
gevent.sleep(0)
print 34
def test2():
print 56
gevent.sleep(0)
print 78
gevent.joinall([
gevent.spawn(test1),
gevent.spawn(test2),
])
-gevent.spawn()”方法会创建一个新的greenlet协程对象,并运行它。”gevent.joinall()”方法会等待所有传入的greenlet协程运行结束后再退出,这个方法可以接受一个”timeout”参数来设置超时时间,单位是秒。
该示例执行顺序如下:
- 先进入协程test1,打印12
- 遇到”gevent.sleep(0)”时,test1被阻塞,自动切换到协程test2,打印56
- 之后test2被阻塞,这时test1阻塞已结束,自动切换回test1,打印34
- 当test1运行完毕返回后,此时test2阻塞已结束,再自动切换回test2,打印78
- 所有协程执行完毕,程序退出
Monkey patching
- 由于Python标准库里的socket是阻塞式的,DNS解析无法并发,包括像urllib库也一样,所以这种情况下用协程完全没意义。解决该问题的比较常见的一种方法是对socket标准库打上猴子补丁(Monkey patching)。
- 使用猴子补丁将socket标准库中的类和方法都会被替换成非阻塞式的,所有其他的代码都不用修改,这样协程的效率就真正体现出来了。Python中其它标准库也存在阻塞的情况,gevent提供了”monkey.patch_all()”方法将所有标准库都替换。
示例:
from gevent import monkey; monkey.patch_socket()
import gevent
def f(n):
for i in range(n):
print gevent.getcurrent(), i
gevent.sleep(0)
g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()
获取协程状态
- 协程状态有已启动和已停止,分别可以用协程对象的”started”属性和”ready()”方法来判断。
- 对于已停止的协程,可以用”successful()”方法来判断其是否成功运行且没抛异常。
- 如果协程执行完有返回值,可以通过”value”属性来获取。另外,greenlet协程运行过程中发生的异常是不会被抛出到协程外的,因此需要用协程对象的”exception”属性来获取协程中的异常。
下面的例子演示各种方法和属性的使用。
# coding:utf8
import gevent
def win():
return 'You win!'
def fail():
raise Exception('You failed!')
winner = gevent.spawn(win)
loser = gevent.spawn(fail)
print(winner.started) # True
print(loser.started) # True
# 在Greenlet中发生的异常,不会被抛到Greenlet外面。
# 控制台会打出Stacktrace,但程序不会停止
try:
gevent.joinall([winner, loser])
except Exception as e:
# 这段永远不会被执行
print
'This will never be reached'
print(winner.ready()) # True
print(loser.ready()) # True
print(winner.value) # 'You win!'
print(loser.value ) # None
print(winner.successful()) # True
print(loser.successful() ) # False
# 这里可以通过raise loser.exception 或 loser.get()
# 来将协程中的异常抛出
print(loser.exception)
参考链接:
python中多进程+协程的使用以及为什么要用它
廖雪峰的博客-gevent
协程
完全理解 Python 迭代对象、迭代器、生成器
Python yield与实现
yield send 知乎
greenlet 详解
python enhanced generator - coroutine
基于协程的Python网络库gevent介绍