c++ 协程_python中的协程有哪些方式实现呢?你知道吗

大家好,我是小梁,今天给大家分享关于操作系统中程序运行很重要的几个概念,而我把它们称为三剑客:进程、线程、协程。

今天主讲协程。

关注我,每天让您获取来自编程的乐趣。

1、概念

1.1 协程

协程:比线程更小的执行单元,又称微线程,在单线程上执行多个任务,自带CPU上下文。用函数切换,开销极小。不通过操作系统调度,没有进程、线程的切换开销。

那么线程与协程有什么区别呢?

我们假设把一个进程比作我们实际生活中的一个拉面馆,负责保持拉面馆运行的服务员就是线程,每个餐桌点菜代表要完成的任务。

当我们用多线程完成任务时,模式是这样的:每来一桌的客人,就在那张桌子上安排一个服务员负责,即有多少桌客人就得对应多少个服务员;

而当我们用协程来完成任务时,模式却有所不同了:就安排一个服务员,来吃饭得有一个点餐和等菜的过程,当A在点菜,就去给B服务,B叫了菜在等待,我就去C,当C也在等菜并且A点菜点完了,赶紧到A来服务… …依次类推。

从上面的例子可以看出,想要使用协程,那么我们的任务必须有等待

当我们要完成的任务是耗时任务时,比如属于IO密集型任务时,我们使用协程来执行任务会节省很多的资源(一个服务员和多个服务员的区别),并且可以极大的利用到系统的资源。

协程,是单线程下的并发,又称微线程,英文名Coroutine。是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置,当程序中存在大量不需要CPU的操作时(IO),适用于协程。【在一个线程中CPU来回切换执行不同的任务,这种现象就是协程】

协程有极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销。

不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

因为协程是一个线程执行,所以想要利用多核CPU,最简单的方法是多进程+协程,这样既充分利用多核,又充分发挥协程的高效率。

那符合什么条件就能称之为协程:

1、必须在只有一个单线程里实现并发

2、修改共享数据不需加锁

3、用户程序里自己保存多个控制流的上下文栈

4、一个协程遇到IO操作自动切换到其它协程

python中对于协程有四个模块,greenlet、gevent、yield和async来实现切换和保存线程

1.2  yield实现任务切换+保存线程

import timedef func1():    while True:        print('func1')        yield '返回func1'def func2():    g = func1()    print(next(g))    for i in range(3):        print(next(g))        time.sleep(3)        print('func2')if __name__ == '__main__':    start = time.time()    func2()    stop = time.time()    print(stop - start)
yield不能节省IO时间,只是单纯的进行程序切换
# 基于yield并发执行,多任务之间来回切换,这就是个简单的协程的体现,# 但是它能够节省I/O时间吗?不能import timedef consumer():    '''任务1:接收数据,处理数据'''    while True:        x = yield        time.sleep(1)  # 发现什么?只是进行了切换,但是并没有节省I/O时间        print('处理了数据:', x)def producer():    '''任务2:生产数据'''    g = consumer()    next(g)  # 找到了consumer函数的yield位置    for i in range(3):        # 给yield传值,然后再循环给下一个yield传值,并且多了切换的程序,        # 比直接串行执行还多了一些步骤,导致执行效率反而更低了。        print('发送了数据:', i)        g.send(i)if __name__ == '__main__':    start = time.time()    # 基于yield保存状态,实现两个任务直接来回切换,即并发的效果    # PS:如果每个任务中都加上打印,那么明显地看到两个任务的打印是你一次我一次,即并发执行的.    producer()  # 在当前线程中只执行了这个函数,但是通过这个函数里面的send切换了另外一个任务    stop = time.time()    print(stop - start)    # 串行执行的方式    print('串行方式')    start = time.time()    res = producer()    consumer()    stop = time.time()    print(stop - start)
执行效果:
发送了数据: 0处理了数据: 0发送了数据: 1处理了数据: 1发送了数据: 2处理了数据: 23.0110418796539307串行方式发送了数据: 0处理了数据: 0发送了数据: 1处理了数据: 1发送了数据: 2处理了数据: 23.0021891593933105

yield检测不到IO,无法实现遇到IO自动切换。

1.3 greenlet是手动切换

# encoding: utf-8from greenlet import greenletimport time"""使用greenlet + switch实现协程调度"""def func1():    print("开门走进卫生间")    time.sleep(3)    gr2.switch()  # 把CPU执行权交给gr2    print("飞流直下三千尺")    time.sleep(3)    gr2.switch()    passdef func2():    print("一看拖把放旁边")    time.sleep(3)    gr1.switch()    print("疑是银河落九天")    passif __name__ == '__main__':    gr1 = greenlet(func1)    gr2 = greenlet(func2)    gr1.switch()  # 把CPU执行权先给gr1
执行效果:
开门走进卫生间一看拖把放旁边飞流直下三千尺疑是银河落九天

greenlet只是提供了一种比yield(生成器)更加便捷的切换方式,当切到一个任务执行时如果遇到IO,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。

1.4 Gevent实现自动切换协程(多协程)

协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。

一般在工作中我们都是进程+线程+协程的方式来实现并发,以达到最好的并发效果。

如果是4核的CPU,一般起5个进程,每个进程中20个线程(5倍CPU数量),每个线程可以起500个协程,大规模爬取页面的时候,等待网络延迟的时间的时候,我们就可以用协程去实现并发。并发数量=520500从而达到5000个并发,这是一般一个4个CPU的机器最大的并发数。nginx在负载均衡的时候最大承载量是5w个。

单线程里的这20个任务的代码通常既有计算操作又有阻塞操作,我们完全可以在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2。。。如此,才能提高效率,这就用到了Gevent模块。

Gevent(自动切换,由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成)。

# encoding: utf-8from gevent import monkey# 将【标准库-阻塞IO实现】替换为【gevent-非阻塞IO实现,即遇到需要等待的IO会自动切换到其它协程monkey.patch_all()import sysimport geventimport requestsimport time'''使用gevent + monkey.patch_all()自动调度网络IO协程'''sys.setrecursionlimit(1000000)  # 增加递归深度def get_page_text(url, order):    print('No{}请求'.format(order), end=' ')    try:        headers = {            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"        }        resp = requests.get(url, headers=headers)  # 发起网络请求,返回需要时间——阻塞IO        html = resp.text        html_len = len(html)        print("%s成功返回:长度为%d" % (url, html_len))        return html_len    except Exception as e:        print('{}发生错误,{}'.format(url, e))        return 0def gevent_joinall():    # spawn是异步提交任务    gevent.joinall([        gevent.spawn(get_page_text, "http://www.sina.com", order=1),        gevent.spawn(get_page_text, "http://www.qq.com", order=2),        gevent.spawn(get_page_text, "http://www.baidu.com", order=3),        gevent.spawn(get_page_text, "http://www.163.com", order=4),        gevent.spawn(get_page_text, "http://www.4399.com", order=5),        gevent.spawn(get_page_text, "http://www.sohu.com", order=6),        gevent.spawn(get_page_text, "http://www.youku.com", order=7),    ])    g_iqiyi = gevent.spawn(get_page_text, "http://www.iqiyi.com", order=8)    g_iqiyi.join()    # #拿到任务的返回值    print('获取返回值', g_iqiyi.value)if __name__ == '__main__':    start = time.time()    time.clock()    gevent_joinall()    end = time.time()    print("over,耗时%d秒" % (end - start))    print(time.clock())
执行效果:
No1请求 No2请求 No3请求 No4请求 No5请求 No6请求 No7请求 http://www.sohu.com成功返回:长度为176910http://www.4399.com成功返回:长度为170880http://www.baidu.com成功返回:长度为260247http://www.qq.com成功返回:长度为229706http://www.sina.com成功返回:长度为527564http://www.163.com成功返回:长度为475453http://www.youku.com成功返回:长度为1324990No8请求 http://www.iqiyi.com成功返回:长度为596791获取返回值 596791over,耗时1秒1.3477432

monkey.patch_all() 一定要放到导入requests模块之前,否则gevent无法识别requests的阻塞。

1.5 async实现协程

# encoding: utf-8import asynciofrom functools import partialimport sysimport requestsimport time'''使用gevent + monkey.patch_all()自动调度网络IO协程'''sys.setrecursionlimit(1000000)  # 增加递归深度async def get_page_text(url, order):    # 使用async创建一个可中断的异步函数    print('No{}请求'.format(order), end=' ')    try:        headers = {            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"        }        # 利用BaseEventLoop.run_in_executor()可以在coroutine中执行第三方的命令,例如requests.get()        # 第三方命令的参数与关键字利用functools.partial传入        future = asyncio.get_event_loop().run_in_executor(None, partial(requests.get, url, headers=headers))        resp = await future        html = resp.text        html_len = len(html)        print("%s成功返回:长度为%d" % (url, html_len))        return html_len    except Exception as e:        print('{}发生错误,{}'.format(url, e))        return 0# 异步函数执行完后回调函数def callback(future):  # 这里默认传入一个future对象    print(future.result())# 异步函数执行完后回调函数,可接收多个参数def callback_2(url, future):  # 传入值的时候,future必须在最后一个    print(url, future.result())def async_run():    # 使用async创建协程    urls = ["http://www.youku.com", "http://www.sina.com", "http://www.qq.com", "http://www.baidu.com",            "http://www.163.com", "http://www.4399.com", "http://www.sohu.com",            ]    loop_task = []    loop = asyncio.get_event_loop()    for i in range(len(urls)):        t = asyncio.ensure_future(get_page_text(urls[i], i + 1))        # t = loop.create_task(task(urls[i], i + 1))        t.add_done_callback(callback)        # t.add_done_callback(partial(callback_2, urls[i]))        loop_task.append(t)    print('等待所有async函数执行完成')    start = time.time()    loop.run_until_complete(asyncio.wait(loop_task))    loop.close()    end = time.time()    print("over,耗时%d秒" % (end - start))if __name__ == '__main__':    async_run()
执行效果:
等待所有async函数执行完成No1请求 No2请求 No3请求 No4请求 No5请求 No6请求 No7请求 http://www.4399.com成功返回:长度为170880170880http://www.sohu.com成功返回:长度为177013177013http://www.baidu.com成功返回:长度为260280260280http://www.qq.com成功返回:长度为229575229575http://www.sina.com成功返回:长度为527564527564http://www.163.com成功返回:长度为475453475453http://www.youku.com成功返回:长度为13356541335654over,耗时1秒

3、关键字:yield

3.1 yield表达式

yield相当于return,只不过return是终结函数并返回一个值,而yield是先把值返回并把函数挂起来,以后还会执行yield以下的语句。

# encoding: utf-8def foo(end_count):    print('yield生成器')    count = 0    while count < end_count:        res = yield count        print('接收到的参数', res)        if res is not None:            count = res        else:            count += 1if __name__ == '__main__':    f = foo(7)    # 第一次调用yield函数是预激活,    # 即调用函数foo时,只执行yield前面的语句,    # 遇到yield就把foo函数挂起来,    # 并返回yield后面附带的值    print(next(f))  # 使用next()调用yield函数    for i in range(3):        # 第二次调用就开始执行yield后面的语句        print(next(f))  # 使用next()调用yield函数    print('*' * 20)    # s使用send给foo函数传值,yield会接收到并赋给res,    print(f.send(5))  # send函数中会执行一次next函数    print(next(f))    # 生成不占空间的列表[0,1,2,3,4,5,6,7,8,9]    # for i in foo(10):    #     print(i)
输出:
    yield生成器    0    接收到的参数 None    1    接收到的参数 None    2    接收到的参数 None    3    ********************    接收到的参数 5    5    接收到的参数 None    6

1、第一次ti调用next函数时,进入foo函数,遇到yield就把count=0返回,并把foo函数挂起

2、在for循环中再次调用next函数时,就开始执行yield后面的赋值语句,由于没有接收到值就默认为None,所以res=None

3、然后接着执行赋值语句后面的打印语句和if判断,由于res为None所以执行count +=1,此时count值为1

4、再次遇到yield,返回1,并把foo函数挂起。

5、send函数是可以给yield生成器传参的,执行send函数时会默认执行一次next函数,原理同上。

到这里你可能就明白yield和return的关系和区别了,带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的,所以调用next的时候,生成器并不会从foo函数的开始执行,只是接着上一步停止的地方开始,然后遇到yield后,return出要生成的数,此步就结束。

为什么用这个生成器,是因为如果用List的话,会占用更大的空间,比如说取0,1,2,3,4,5,6............1000

你可能会这样:这个时候range(1000)就默认生成一个含有1000个数的list了,所以很占内存。

这个时候你可以用刚才的yield组合成生成器进行实现

for i in foo(10000):    print(i)
但这个由于每次都要调用函数foo,所以比较耗时间。【这就是用时间换空间】

4、关键字:async/await

asyncio 是用来编写 并发 代码的库,使用 async/await 语法。

asyncio 被用作多个提供高性能 Python 异步框架的基础,包括网络和网站服务,数据库连接库,分布式任务队列等等。

asyncio 往往是构建 IO 密集型和高层级 结构化 网络代码的最佳选择。

正常的函数在执行时是不会中断的,所以你要写一个能够中断的函数,就需要添加async关键。

async 用来声明一个函数为异步函数,异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等到挂起条件(假设挂起条件是asyncio.sleep(5))消失后,也就是5秒到了再回来执行。

await 用来用来声明程序挂起,比如异步程序执行到某一步时需要等待的时间很长,就将此挂起,去执行其他的异步程序。await 后面只能跟异步程序或有await属性的对象,因为异步程序与一般程序不同。假设有两个异步函数async a,async b,a中的某一步有await,当程序碰到关键字await b()后,异步程序挂起后去执行另一个异步b程序,就是从函数内部跳出去执行其他函数,当挂起条件消失后,不管b是否执行完,要马上从b程序中跳出来,回到原程序执行原来的操作。如果await后面跟的b函数不是异步函数,那么操作就只能等b执行完再返回,无法在b执行的过程中返回。如果要在b执行完才返回,也就不需要用await关键字了,直接调用b函数就行。所以这就需要await后面跟的是异步函数了。在一个异步函数中,可以不止一次挂起,也就是可以用多个await。

可以使用async、await来实现协程的并发,下面以一个爬虫例子来说明:

# encoding: utf-8from gevent import monkeymonkey.patch_all()import geventimport asynciofrom functools import wraps, partialimport timeimport requests# 定义一个查看函数执行时间的装饰器def func_use_time(func):    @wraps(func)    def inside(*arg, **kwargs):        start = time.clock()        res = func(*arg, **kwargs)        print('***************执行时间*****************', time.clock() - start)        return res    return insidedef get_page_text(url):    # 爬取网站    print(url)    try:        headers = {            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"        }        resp = requests.get(url, headers=headers)  # 发起网络请求,返回需要时间——阻塞IO        html = resp.text        return html    except Exception as e:        print('{}发生错误,{}'.format(url, e))        return ''class Narmal():    # 正常爬取    def __init__(self, urls):        self.urls = urls        self.res_dict = {}    @func_use_time    def run(self):        for url in self.urls:            res = get_page_text(url)            self.res_dict[url] = len(res)        print('串行获取结果', self.res_dict)class UseAsyncio():    # 使用async实现协程并发    def __init__(self, urls):        self.urls = urls        self.res_dict = {}    # 定义一个异步函数,执行爬取任务    async def task(self, url):        print(url)        try:            headers = {                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"            }            # 利用BaseEventLoop.run_in_executor()可以在coroutine中执行第三方的命令,例如requests.get()            # 第三方命令的参数与关键字利用functools.partial传入            future = asyncio.get_event_loop().run_in_executor(None, partial(requests.get, url, headers=headers))            resp = await future            html = resp.text            self.res_dict[url] = len(html)            return html        except Exception as e:            print('{}发生错误,{}'.format(url, e))            return ''    @func_use_time    def run(self):        loop = asyncio.get_event_loop()        tasks = [asyncio.ensure_future(self.task(url)) for url in self.urls]        loop.run_until_complete(asyncio.wait(tasks))        loop.close()        # 获取async结果        # for task in tasks:        #     print(task.result())        print('async获取结果', self.res_dict)class UseGevent():    # 使用Gevent实现协程并发    def __init__(self, urls):        self.urls = urls        self.res_dict = {}    def task(self, url):        res = get_page_text(url)        self.res_dict[url] = len(res)    @func_use_time    def run(self):        gevent.joinall([gevent.spawn(self.task, url) for url in self.urls])        print(self.res_dict)if __name__ == '__main__':    urls = ["http://www.sina.com", "http://www.qq.com", "http://www.baidu.com",            "http://www.163.com", "http://www.4399.com", "http://www.sohu.com",            "http://www.youku.com",            ]    print("使用正常爬取方式,即串行")    Narmal(urls).run()    print("使用Asyncio爬取方式,async实现协程并发")    UseAsyncio(urls).run()    print("使用Gevent爬取方式,实现协程并发")    UseGevent(urls).run()

输出:

使用正常爬取方式,即串行    http://www.sina.com    http://www.qq.com    http://www.baidu.com    http://www.163.com    http://www.4399.com    http://www.sohu.com    http://www.youku.com    串行获取结果 {'http://www.sina.com': 539723, 'http://www.qq.com': 227753, 'http://www.baidu.com': 166916, 'http://www.163.com': 483531, 'http://www.4399.com': 172837, 'http://www.sohu.com': 178312, 'http://www.youku.com': 990760}    ***************执行时间***************** 1.9352532    使用Asyncio爬取方式,async实现协程并发    http://www.sina.com    http://www.qq.com    http://www.baidu.com    http://www.163.com    http://www.4399.com    http://www.sohu.com    http://www.youku.com    async获取结果 {'http://www.4399.com': 172837, 'http://www.163.com': 483531, 'http://www.qq.com': 227753, 'http://www.sohu.com': 178310, 'http://www.baidu.com': 166625, 'http://www.sina.com': 539723, 'http://www.youku.com': 1047892}    ***************执行时间***************** 0.951011    使用Gevent爬取方式,实现协程并发    http://www.sina.com    http://www.qq.com    http://www.baidu.com    http://www.163.com    http://www.4399.com    http://www.sohu.com    http://www.youku.com    {'http://www.163.com': 483531, 'http://www.4399.com': 172837, 'http://www.sohu.com': 178312, 'http://www.qq.com': 227753, 'http://www.baidu.com': 166760, 'http://www.sina.com': 539723, 'http://www.youku.com': 994926}    ***************执行时间***************** 1.0057508

相对来说还是使用async执行效率高些

【后记】为了让大家能够轻松学编程,我创建了一个公众号【轻松学编程】,里面有让你快速学会编程的文章,当然也有一些干货提高你的编程水平,也有一些编程项目适合做一些课程设计等课题。

也可加我微信【1257309054】,拉你进群,大家一起交流学习。如果文章对您有帮助,请我喝杯咖啡吧!

381d96510834bb3a3cdbb2fa116b9b1f.png

您觉得文章有帮助,点个在看吧!谢谢^_^

a1ed4512b94dc3476f103a5fd513e8f0.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值