先介绍下什么是协程:
协程,又称微线程,纤程,英文名coroutine。协程的作用,是在执行函数a时,可以随时中断,去执行函数b,然后中断继续执行函数a(可以自由切换)。但这一过程并不是函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行。
是不是有点没看懂,没事,我们下面会解释。要理解协程是什么,首先需要理解yield,这里简单介绍下,yield可以理解为生成器,yield item这行代码会产出一个值,提供给next(...)的调用方; 此外,还会作出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用next()。调用方会从生成器中拉取值,但是在协程中,yield关键字一般是在表达式右边(如,data=yield),协程可以从调用方接收数据,也可以产出数据,下面看一个简单的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
>>> 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来确定,如下:
1
2
3
|
>>> import inspect >>> inspect.getgeneratorstate(my_co) 'gen_closed' |
这里再解释下next(my_co),如果在创建好协程对象之后,立即把none之外的值发送给它,会出现如下错误:
1
2
3
4
5
6
7
8
|
>>> 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 表达式,准备好作为活跃的协程使用)。
再参考下面这个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
>>> 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跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
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,在开始执行之后:
- 调用next(c)启动生成器;
- 进入do_work,这是一个递归调用,其内部将url传递给consumer,由consumer来发出请求,获取到html信息,返回给produce,
- produce解析html,获取url数据,继续生产url,
- 当所有的url都在history_urls中,也就是说我们已经爬取了所有的url地址,结束递归调用
- 调用c.close(),关闭consumer,整个过程结束。
可以看到,我们的整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对服务器之家的支持。
原文链接:https://www.lylinux.net/article/2019/1/13/54.html