协程就是充分利用cpu给该线程的时间,在一个线程中放多个任务,某个任务遇到阻塞,执行下一个任务。特点是记住这些任务执行到哪里了。如果一个线程一个任务,比较容易进入阻塞队列,如果这条线程永远在工作,永远不会进入阻塞队列。
cpu虽然可以分时操作,但是能开启的进程是有限的,尽管线程比较轻量,一个cpu同一时刻只能处理一个线程。如果我要处理的任务是无限,如50000个,假如开了200个线程,这200个线程都阻塞了,那下面的4万多个都动不了。当然,如果一个ie线程中没有IO阻塞,只有计算,cpu就会得到充分利用。但是实际情况中往往IO阻塞非常多,如果阻塞程序就停止,就不能做其他事情了,虽然操作系统会调度其他进程或线程工作,但是当前的进程还是会有分配给他的时间片,而他实际是阻塞时还占用着cpu,这是对cpu的浪费。
而进程,线程都会占用系统资源,在他们之间切换也会浪费一些时间,所以在高并发越来越重要的今天,使用线程或进程就不能满足我们了。
所以有了协程:也叫纤程,对于cpu来说,线程是他执行的最小单位,也就是说他只能看到线程,协程是看不到的。
一条线程 在多个任务之间来回切换,切换这个动作是浪费时间的。对于cpu,操作系统来说,协程是不存在的,他们只管执行线程。他们不管你执行哪个任务,只管执行线程的指令。
进程,线程,协程的主要目的是提高效率。而线程和进程是抢占式的程序,在什么时间哪个线程或进程使用cpu是操作系统决定的。操作系统层面我们是无法控制的。而协程是用户可以调度谁先谁后的。yield是协程的一个最底层的实现。
例 1:
# 线程也好进程也好,都把任务交到一个函数里面执行了
def my_generator(): # 可以想象当前这个函数也是一个任务,只不过我把他放到当前这个程序当中同一条线程(主线程)执行了。
for i in range(10):
yield i # 这个任务执行一点,下面哪个任务执行一点。在此for循环之间来回切换。
# yield 执行的是事情是保持当前程序的状态。所以拿到的结果是两个程序之间交替切换拿到的。
for num in my_generator():
print(num)
运行结果:
0
1
2
3
4
5
6
7
8
9
例2:
# 最简单的生产者消费者模型,生产一个消费一个
def consumer():
for num in producer():
print(num)
def producer():
for i in range(1000):
yield i
consumer()
例3:
import time
import queue
def consumer(name):
print('--->ready to eat humburger...') # 一个消费者准备吃
while True:
new_humberger = yield # # 没有send就挂起来,等相当于阻塞。有send给yield才能往下走;
print('[%s] is eating humberger %s' % (name, new_humberger)) # 反复打印名字和接收的值
time.sleep(5)
def producer():
r = con.__next__() # 进入con迭代器执行了第一遍,
r = con2.__next__()
n = 0
while 1: # 死循环
time.sleep(5) # 停一秒
print('\033[32;1m[producer]\033[0m is making humberger %s and %s ' % (n, n + 1)) # 生产者已经做出来两个humberer ,如0号和1号
con.send(n) # 告诉你我已经做好了,通过send发给你
con2.send(n + 1)
n += 2 # 每两个两个地生成,如前一次是0和1,下次就是2和3
if __name__ == '__main__':
con = consumer('c1') # 一个消费者对象。con为迭代器对象,生成了两个对象而已,什么也没做,genatator object consumer
con2 = consumer('c2') # 消费者对象。genarator object consumer
producer()
一般早函数之间切换要通过next,send。python对协程的底层实现进行了封装:Greenlet,帮助我们在各个函数直接切换更方便。
在yield在任务之间来回切换时也是要时间的。这样不仅没有增强效率,反而时间增多。为了更好的利用协程,协程的优势是:能把一个线程的执行能明确的切分开。能帮你记住哪个任务执行到哪个位置上了,并且实现安全的切换。
正常情况下切换任务是不会提高效率的。但一个任务不得不陷入阻塞了。在这个任务阻塞的过程中切换到另一个任务继续执行。你的程序只要还有任务需要执行。你的当前线程永远不会阻塞。
如果有多线程,一个线程的任务为做饭,一个线程的任务为洗衣服,做饭的如果阻塞了不会影响洗衣服的人。洗衣服的人也不会影响做饭的人。现有两个线程。假如要做4件事情。还有打扫,洗碗。这4件事情只有两个人做。做饭的时候如果阻塞了还会做其他的事吗?答案是不可以。也就是说一个线程阻塞了不可以做别的事了。只能在cpu等着。阻塞完了才可以做下一件事。现在的问题是阻塞白阻塞在这了。cpu是闲着的。所有为了使cpu能做更多的事情。只能靠开启无限的线程去做其他事实现并发效果。而开启很多线程成本比较高。而协程是在一个线程中开启这四个任务。当做一个任务做到一半时,就可以切到另外一个任务中。然后再切换回来。这个过程时协程帮助你完成的。也就是说协程能完成在不同的任务间切换。这是一个线程做不到的。
当你的程序陷入阻塞就切换到下一个任务执行。假设有100个任务。这100个任务执行完了才陷入阻塞,相当于在程序执行这100个任务的过程中。程序是不会阻塞的。而cpu是看不见协程的概念的。那对于线程来讲永远在忙碌。那么不会进入阻塞队列了,相当于线程给cpu造成了一个假象。让cpu觉得你的程序一直在忙,是一个高计算的程序而不是一个高阻塞的程序。这样的化cpu会分更多时间片给当前这个线程。做跟多操作。
所以协程的好处是模拟让一个线程做很多工作的场景。让这个线程占用更多cpu。而阻塞的时间是大家共同利用的。如一个线程中多个协程是阻塞的。那么可以利用这些阻塞的时间做别的暂时非阻塞的事情。
应用场景:爬虫,如果要访问5000个url,可以启动5000个线程。但开启线程需要消耗时间。而协程的切换是以python代码切换的,线程的切换以操作系统级别切换。
协程模块
greenlet: gevent底层,帮助实现协程的切换的模块。
gevent: 在greenlet基础上封装的,所以比greenlet更强大。能提供更全面的功能。
class greenlet(object):
Creates a new greenlet object (without running it).
def switch(self, *args, **kwargs)
例
from greenlet import greenlet # greenlet模块中greenlet类
# 边吃边玩
def eat():
print('eating1')
g2.switch() # 执行完上一句切换到g2执行
print('eating2')
def play():
print('playing1')
print('playing2')
g1.switch() # switch做到了想在哪切换就在哪切换。想切换到哪个程序就切换到哪个程序对应的对象进行switch切换。
g1 = greenlet(eat) # 类对象,参数为函数名
g2 = greenlet(play)
g1.switch() # 切换到g1里面注册的方法开始执行。如果eat方法没有放任何的切换节点。就会从头执行到尾。
执行结果
eating1
playing1
playing2
eating2
gevent 模块
单线程的并发效果
from gevent import monkey
monkey.patch_all() # 把文件中的阻塞事件都检测到,并打包,也就是他认识文件中的打包方式
import time # time里面的sleep方法gevent模块就认识了
import gevent
def eat():
print('eating 1')
time.sleep(5)
print('eating 2')
return 'eat finished'
def play():
print('playing 1')
time.sleep(5)
print('playing 2')
return 'play finished'
g1 = gevent.spawn(eat) # 自动检测阻塞事件,遇见阻塞了就会进行切换。有些阻塞它不认识。
g2 = gevent.spawn(play)
# g1.join() # 阻塞直到g1结束
# g2.join() # 阻塞直到g2结束
# 或
gevent.joinall([g1, g2]) # 相当于完成列表中全部阻塞了
print(g1.value) # 获取一个协程的返回值
print(g2.value)
运行结果
eating 1
playing 1
eating 2
playing 2
eat finished
play finished
用协程爬取网站
from gevent import monkey; monkey.patch_all()
import time
import gevent
import requests
url_lis = [
'http://www.baidu.com',
'http://www.4399.com',
'http://www.sougou.com',
'http://www.sohu.com',
'http://www.sina.com',
'http://www.jd.com',
'http://www.douban.com',
'http://www.baidu.com',
'http://www.4399.com',
'http://www.sougou.com',
'http://www.sohu.com',
'http://www.sina.com',
'http://www.jd.com',
'http://www.douban.com',
]
def get_url(url):
response = requests.get(url) # 访问一个网时的回复
if response.status_code == 200: # 访问一个网页给你返回的一个状态码 200 正常,其他都是错误的。
print(url, len(response.text)) # 完整的网页
start = time.time()
g_lst = []
for url in url_lis:
g = gevent.spawn(get_url, url)
g_lst.append(g)
gevent.joinall(g_lst)
print(time.time() - start) # 没用协程的时间: 2.0402512550354004
运行结果
http://www.baidu.com 2381
http://www.baidu.com 2381
http://www.sohu.com 176399
http://www.sohu.com 176399
http://www.jd.com 109083
http://www.4399.com 179199
http://www.jd.com 109083
http://www.sougou.com 30659
http://www.sougou.com 30659
http://www.4399.com 179199
http://www.sina.com 571721
http://www.sina.com 571721
http://www.douban.com 89247
http://www.douban.com 89254
0.6063692569732666