python多线程和协程

  • 任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程。多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量。
  • Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程在执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码上了锁。所以,多线程在Python中只能交替执行,并不能做到真正的并发执行。
  • 所以在python中,通常使用协程来代替多线程。

协程

  • 协程是一种用户级的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
  • 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
  • 在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其它协程共享全局数据和其它资源。
  • 协程是用户自己来编写调度逻辑的,对CPU来说,协程其实是单线程,所以CPU不用去考虑怎么调度、切换上下文,这就省去了CPU的切换开销,所以协程在一定程度上又好于多线程。
协程基础

Python对协程的支持是通过generator实现的。generator也叫生成器。何谓生成器呢?就得先从另外一个叫迭代器的对象说起:

迭代器
  • 迭代器是一个带状态的对象,调用next()方法的时候返回容器中的下一个值,任何实现了iternext()方法的对象都是迭代器,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”参数来设置超时时间,单位是秒。

该示例执行顺序如下:

  1. 先进入协程test1,打印12
  2. 遇到”gevent.sleep(0)”时,test1被阻塞,自动切换到协程test2,打印56
  3. 之后test2被阻塞,这时test1阻塞已结束,自动切换回test1,打印34
  4. 当test1运行完毕返回后,此时test2阻塞已结束,再自动切换回test2,打印78
  5. 所有协程执行完毕,程序退出

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介绍

你可能感兴趣的:(python多线程和协程)