协程

协程就是充分利用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
  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值