python中的协程深入理解

先介绍下什么是协程:

  协程,又称微线程,纤程,英文名Coroutine。协程的作用,是在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换)。但这一过程并不是函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行。

是不是有点没看懂,没事,我们下面会解释。要理解协程是什么,首先需要理解yield,这里简单介绍下,yield可以理解为生成器,yield item这行代码会产出一个值,提供给next(...)的调用方; 此外,还会作出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用next()。调用方会从生成器中拉取值,但是在协程中,yield关键字一般是在表达式右边(如,data=yield),协程可以从调用方接收数据,也可以产出数据,下面看一个简单的例子:

>>> def simple_coroutine():
...  print('coroutine start')
...  x = yield
...  print('coroutine recive:',x)
...
>>> my_co=simple_coroutine()
>>> my_co
<generator object simple_coroutine at 0x1085174f8>
>>> next(my_co)
coroutine start
>>> my_co.send(42)
coroutine recive: 42
Traceback (most recent call last):
 File "<input>", line 1, in <module>
StopIteration

其中x = yield就是精髓部分,意思是从客户端获取数据,产出None,因为yield关键字右边没有表达式, 而协程在创建完成之后,是没有启动的,没有在yield处暂停,所以需要调用next()函数,启动协程,在调用my_co.send(42)之后,协程定义体中的yield表达式会计算出42,现在协程恢复,一直运行到下一个yield表达式,或者终止,在最后,控制权流动到协程定义体的末尾,生成器抛出StopIteration异常。

协程有四个状态,如下:

  • 'GEN_CREATED' 等待开始执行。
  • 'GEN_RUNNING' 解释器正在执行。
  • 'GEN_SUSPENDED' 在 yield 表达式处暂停。
  • 'GEN_CLOSED' 执行结束。

当前状态可以使用inspect.getgeneratorstate来确定,如下:

>>> import inspect
>>> inspect.getgeneratorstate(my_co)
'GEN_CLOSED'

这里再解释下next(my_co),如果在创建好协程对象之后,立即把None之外的值发送给它,会出现如下错误:

>>> my_co=simple_coroutine()
>>> my_co.send(42)
Traceback (most recent call last):
 File "<input>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator
>>> my_co=simple_coroutine()
>>> my_co.send(None)
coroutine start

最先调用 next(my_co) 函数这一步通常称为“预激”(prime)协程(即,让协程向前执行到第一个 yield 表达式,准备好作为活跃的协程使用)。

再参考下面这个例子:

>>> def simple_coro2(a):
...  print('-> Started: a =', a)
...  b = yield a
...  print('-> Received: b =', b)
...  c = yield a + b
...  print('-> Received: c =', c)
...
>>> my_coro2 = simple_coro2(14)
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(my_coro2)
'GEN_CREATED'
>>> next(my_coro2) # 协程执行到`b = yield a`处暂停,等待为b赋值,
-> Started: a = 14
14
>>> getgeneratorstate(my_coro2)
'GEN_SUSPENDED' #从状态也可以看到,当前是暂停状态。
>>> my_coro2.send(28) #将28发送到协程,计算yield表达式,并把结果绑定到b,产出a+b的值,然后暂停。
-> Received: b = 28
42
>>> my_coro2.send(99)
-> Received: c = 99
Traceback (most recent call last):
 File "<input>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2)
'GEN_CLOSED'

simple_coro2的执行过程如下图所示:

  • 调用next(my_coro2),打印第一个消息,然后执行yield a,产出数字 14。
  • 调用my_coro2.send(28),把28赋值给b,打印第二个消息,然后执行yield a + b,产 出数字 42。
  • 调用my_coro2.send(99),把 99 赋值给 c,打印第三个消息,协程终止。

说了这么多,我们为什么要用协程呢,下面我们再看看它的优势是什么:

  • 执行效率极高,因为子程序切换(函数)不是线程切换,由程序自身控制,没有切换线程的开销。所以与多线程相比,线程的数量越多,协程性能的优势越明显。
  • 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁,因此执行效率高很多。

说明:协程可以处理IO密集型程序的效率问题,但是处理CPU密集型不是它的长处,如要充分发挥CPU利用率可以结合多进程+协程。

下面看最后一个例子,传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。

如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

from bs4 import BeautifulSoup
import requests
from urllib.parse import urlparse

start_url = 'https://www.cnblogs.com'
trust_host = 'www.cnblogs.com'
ignore_path = []
history_urls = []

def parse_html(html):
  soup = BeautifulSoup(html, "lxml")
  print(soup.title)
  links = soup.find_all('a', href=True)
  return (a['href'] for a in links if a['href'])

def parse_url(url):
  url = url.strip()

  if url.find('#') >= 0:
    url = url.split('#')[0]
  if not url:
    return None
  if url.find('javascript:') >= 0:
    return None

  for f in ignore_path:
    if f in url:
      return None
  if url.find('http') < 0:
    url = start_url + url
    return url
  parse = urlparse(url)
  if parse.hostname == trust_host:
    return url

def consumer():
  html = ''
  while True:
    url = yield html
    if url:
      print('[CONSUMER] Consuming %s...' % url)
      rsp = requests.get(url)
      html = rsp.content

def produce(c):
  next(c)

  def do_work(urls):
    for u in urls:
      if u not in history_urls:
        history_urls.append(u)
        print('[PRODUCER] Producing %s...' % u)
        html = c.send(u)
        results = parse_html(html)
        work_urls = (x for x in map(parse_url, results) if x)
        do_work(work_urls)

  do_work([start_url])
  c.close()

if __name__ == '__main__':
  c = consumer()
  produce(c)
  print(len(history_urls))

首先consumer函数是一个generator,在开始执行之后:

  1. 调用next(c)启动生成器;
  2. 进入do_work,这是一个递归调用,其内部将url传递给consumer,由consumer来发出请求,获取到html信息,返回给produce,
  3. produce解析html,获取url数据,继续生产url,
  4. 当所有的url都在history_urls中,也就是说我们已经爬取了所有的url地址,结束递归调用
  5. 调用c.close(),关闭consumer,整个过程结束。

可以看到,我们的整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • 深入浅析python中的多进程、多线程、协程

    进程与线程的历史 我们都知道计算机是由硬件和软件组成的.硬件中的CPU是计算机的核心,它承担计算机的所有任务. 操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配.任务的调度. 程序是运行在系统上的具有某种功能的软件,比如说浏览器,音乐播放器等. 每次执行程序的时候,都会完成一定的功能,比如说浏览器帮我们打开网页,为了保证其独立性,就需要一个专门的管理和控制执行程序的数据结构--进程控制块. 进程就是一个程序在一个数据集上的一次动态执行过程. 进程一般由程序.数据集.进程控

  • python协程用法实例分析

    本文实例讲述了python协程用法.分享给大家供大家参考.具体如下: 把函数编写为一个任务,从而能处理发送给他的一系列输入,这种函数称为协程 def print_matchs(matchtext): print "looking for",matchtext while True: line = (yield) #用 yield语句并以表达式(yield)的形式创建协程 if matchtext in line: print line >>> matcher = pr

  • Tornado协程在python2.7如何返回值(实现方法)

    错误写法 class RemoteHandler(web.RequestHandler): @gen.coroutine def get(self): response = httpclient('http://www.baidu.com') self.write(response.body) @gen.coroutine def httpClient(url): result = yield httpclient.AsyncHTTPClient().fetch(url) return resu

  • python简单线程和协程学习心得(分享)

    python中对线程的支持的确不够,不过据说python有足够完备的异步网络框架模块,希望日后能学习到,这里就简单的对python中的线程做个总结 threading库可用来在单独的线程中执行任意的python可调用对象.尽管此模块对线程相关操作的支持不够,但是我们还是能够用简单的线程来处理I/O操作,以减低程序响应时间. from threading import Thread import time def countdown(n): while n > 0: print('T-minus:

  • Python并发编程协程(Coroutine)之Gevent详解

    Gevent官网文档地址:http://www.gevent.org/contents.html 基本概念 我们通常所说的协程Coroutine其实是corporateroutine的缩写,直接翻译为协同的例程,一般我们都简称为协程. 在linux系统中,线程就是轻量级的进程,而我们通常也把协程称为轻量级的线程即微线程. 进程和协程 下面对比一下进程和协程的相同点和不同点: 相同点: 我们都可以把他们看做是一种执行流,执行流可以挂起,并且后面可以在你挂起的地方恢复执行,这实际上都可以看做是con

  • python编程使用协程并发的优缺点

    协程 协程是一种用户态的轻量级线程,又称微线程. 协程拥有自己的寄存器上下文和栈,调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈.因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置. 优点: 1.无需线程上下文切换的开销 2.无需原子操作锁定及同步的开销 3.方便切换控制流,简化编程模型 4.高并发+高扩展性+低成本:一个CPU支持上万的协程都不

  • python线程、进程和协程详解

    引言 解释器环境:python3.5.1 我们都知道python网络编程的两大必学模块socket和socketserver,其中的socketserver是一个支持IO多路复用和多线程.多进程的模块.一般我们在socketserver服务端代码中都会写这么一句: server = socketserver.ThreadingTCPServer(settings.IP_PORT, MyServer) ThreadingTCPServer这个类是一个支持多线程和TCP协议的socketserver

  • python并发编程之多进程、多线程、异步和协程详解

    最近学习python并发,于是对多进程.多线程.异步和协程做了个总结. 一.多线程 多线程就是允许一个进程内存在多个控制权,以便让多个函数同时处于激活状态,从而让多个函数的操作同时运行.即使是单CPU的计算机,也可以通过不停地在不同线程的指令间切换,从而造成多线程同时运行的效果. 多线程相当于一个并发(concunrrency)系统.并发系统一般同时执行多个任务.如果多个任务可以共享资源,特别是同时写入某个变量的时候,就需要解决同步的问题,比如多线程火车售票系统:两个指令,一个指令检查票是否卖完

  • python 生成器协程运算实例

    一.yield运行方式 我们定义一个如下的生成器: def put_on(name): print("Hi {}, 货物来了,准备搬到仓库!".format(name)) while True: goods = yield print("货物[%s]已经被%s搬进仓库了."%(goods,name)) p = put_on("bigberg") #输出 G:\python\install\python.exe G:/python/untitled

  • 简述Python中的进程、线程、协程

    进程.线程和协程之间的关系和区别也困扰我一阵子了,最近有一些心得,写一下. 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度. 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的). 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度. 进程和其他两个的区别还是很明显的. 协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力. Pyt

随机推荐