什么是协程?与线程和进程对比优劣在哪?gevent协程示例代码
协程
协程,又称微线程,纤程。英文名Coroutine。。一句话说明什么是线程:协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。
因此:
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
如何理解:
程其实可以认为是比线程更小的执行单元。 为啥说他是一个执行单元,因为他自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。
通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定
协程和线程差异
那么这个过程看起来比线程差不多。其实不然, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
协程的问题
但是协程有一个问题,就是系统并不感知,所以操作系统不会帮你做切换。 那么谁来帮你做切换?让需要执行的协程更多的获得CPU时间才是问题的关键。
例子
目前的协程框架一般都是设计成 1:N 模式。所谓 1:N 就是一个线程作为一个容器里面放置多个协程。 那么谁来适时的切换这些协程?答案是有协程自己主动让出CPU,也就是每个协程池里面有一个调度器, 这个调度器是被动调度的。意思就是他不会主动调度。而且当一个协程发现自己执行不下去了(比如异步等待网络的数据回来,但是当前还没有数据到), 这个时候就可以由这个协程通知调度器,这个时候执行到调度器的代码,调度器根据事先设计好的调度算法找到当前最需要CPU的协程。 切换这个协程的CPU上下文把CPU的运行权交个这个协程,直到这个协程出现执行不下去需要等等的情况,或者它调用主动让出CPU的API之类,触发下一次调度。
那么这个实现有没有问题?
其实是有问题的,假设这个线程中有一个协程是CPU密集型的他没有IO操作, 也就是自己不会主动触发调度器调度的过程,那么就会出现其他协程得不到执行的情况, 所以这种情况下需要程序员自己避免。这是一个问题,假设业务开发的人员并不懂这个原理的话就可能会出现问题。
缺点:
- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
- 进行阻塞(Blocking)操作(如IO时)会阻塞掉单线程的整个程序
协程的好处
在IO密集型的程序中由于IO操作远远慢于CPU的操作,所以往往需要CPU去等IO操作。 同步IO下系统需要切换线程,让操作系统可以在IO过程中执行其他的东西。 这样虽然代码是符合人类的思维习惯但是由于大量的线程切换带来了大量的性能的浪费,尤其是IO密集型的程序。
所以人们发明了异步IO。就是当数据到达的时候触发我的回调。来减少线程切换带来性能损失。 但是这样的坏处也是很大的,主要的坏处就是操作被 “分片” 了,代码写的不是 “一气呵成” 这种。 而是每次来段数据就要判断 数据够不够处理哇,够处理就处理吧,不够处理就在等等吧。这样代码的可读性很低,其实也不符合人类的习惯。
但是协程可以很好解决这个问题。比如 把一个IO操作 写成一个协程。当触发IO操作的时候就自动让出CPU给其他协程。要知道协程的切换很轻的。 协程通过这种对异步IO的封装 既保留了性能也保证了代码的容易编写和可读性。在高IO密集型的程序下很好。但是高CPU密集型的程序下没啥好处。
总结起来有好处如下几点:
- 无需线程上下文切换的开销
- 无需原子操作锁定及同步的开销
- "原子操作(atomic operation)是不需要synchronized",所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。
- 方便切换控制流,简化编程模型
- 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
适用性:
计算密集型:多进程
IO密集型:多线程、协程
简单的协程代码示例(python2/3中有所不同):
#!/usr/bin/env python2/3 # -*- coding:utf-8 -*- # @Time: 2020/7/5 17:14 # @Author:zhangmingda # @File: 简易协程代码示例.py # @Software: PyCharm # Description: import time def funA(): while True: print('----A----') t = yield time.sleep(0.5) print('----A-done----') return t def funB(fun): while True: print('----B----') fun.next() # python3中为:__next__() time.sleep(0.5) print('----B-done----') if __name__ == '__main__': # yield 执行卡住,等待被调用.next()才能继续进行 a = funA() print("函数A执行完毕,返回值", a, type(a)) funB(a)
运行结果:
python2 简易协程代码示例.py ('\xe5\x87\xbd\xe6\x95\xb0A\xe6\x89\xa7\xe8\xa1\x8c\xe5\xae\x8c\xe6\xaf\x95\xef\xbc\x8c\xe8\xbf\x94\xe5\x9b\x9e\xe5\x80\xbc', <generator object funA at 0x7f090f5aefa0>, <type 'generator'>) ----B---- ----A---- ----B-done---- ----B---- .....省略......
协程-greenlet版(py2中的)
为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单
安装方式
使用如下命令安装greenlet模块:
sudo pip install greenlet
#!/usr/bin/env python2 # -*- coding:utf-8 -*- # @Time: 2020/7/5 17:38 # @Author:zhangmingda # @File: greenlet版本协程测试.py # @Software: PyCharm # Description: from greenlet import greenlet import time def test1(): while True: print("---A--") gr2.switch() time.sleep(0.5) def test2(): while True: print("---B--") gr1.switch() time.sleep(0.5) gr1 = greenlet(test1) gr2 = greenlet(test2) #切换到gr1中运行 gr1.switch()
输出
python greenlet版本协程测试.py ---A-- ---B-- ---A-- ---B-- ---A-- .......省略.....
greenlet已经实现了协程,但是这个还的人工切换,是不是觉得太麻烦了,不要捉急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent
其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
gevent(Python2、3都行)
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有线程在运行。
遇到耗时等待的就把控制权交出去,让当前可以执行的协程执行
tornado 就是使用gevent开发的项目。
注意:
1、不能使用time.sleep()模拟耗时操作,gevent.sleep。
2、如果有网络交互也需要使用gevent里面的socket;即from gevent import socket
3、有IO操作的时候需要把gevent 里面的monkey类导入执行,进行打补丁monkey.patch_all() :实际是将所有IO操作单独做了标记,便于gevent监控所有的IO是否可以执行下一步;python是动态语言,这过程中已经在运行中修改了我们写的代码
gevent测试代码一(gevent.sleep模拟多任务IO耗时):
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # @Time: 2020/7/5 17:45 # @Author:zhangmingda # @File: gevent协程代码测试.py # @Software: PyCharm # Description: import gevent,time def f(n): for i in range(n): print(gevent.getcurrent(), i) gevent.sleep(1) g1 = gevent.spawn(f, 5) g2 = gevent.spawn(f, 5) g3 = gevent.spawn(f, 5) g1.join() g2.join() g3.join() # #效果:三个IO操作(gevent.sleep()同时进行,谁的先完成,谁就先继续往下执行 # #gevent模块会自动检测什么是IO操作
输出效果
<Greenlet at 0x20d023dee18: f(5)> 0
<Greenlet at 0x20d023ded08: f(5)> 0
<Greenlet at 0x20d0277f048: f(5)> 0
<Greenlet at 0x20d023dee18: f(5)> 1
<Greenlet at 0x20d023ded08: f(5)> 1
<Greenlet at 0x20d0277f048: f(5)> 1
......省略......
示例代码二(gevent+urllib并发爬虫):
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # @Time: 2020/7/5 18:28 # @Author:zhangmingda # @File: gevent_urlopen协程爬虫示例.py # @Software: PyCharm # Description: from urllib.request import urlopen import gevent from gevent import monkey # 在所有代码前,先打gevent 的补丁 monkey.patch_all() # 定义单独执行get任务的函数 def get(url): print("GET:%s" % url) # urlopen() 在获取网页,是耗时的IO阻塞操作 get_result = urlopen(url) data = get_result.read() print("%d bytes received from URL: %s" % (len(data), url)) # gevent.spawn()添加协程任务到队列中;spawn下蛋的意思 # spawn() 接收任务函数和参数 get_request_list = [ gevent.spawn(get,'https://www.python.org'), gevent.spawn(get,'https://www.yahoo.com'), gevent.spawn(get,'https://github.com') ] gevent.joinall(get_request_list)
输出效果:
GET:https://www.python.org
GET:https://www.yahoo.com
GET:https://github.com
134037 bytes received from URL: https://github.com
48901 bytes received from URL: https://www.python.org
|雅虎的许久无返回卡住了,而其他两个网站感官上同时返回了结果
示例代码三(gevent版-TCP服务器):
采用模块:gevent;from gevent import socket, monkey
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # @Time: 2020/7/5 19:31 # @Author:zhangmingda # @File: gevent_socket_tcp_server.py # @Software: PyCharm # Description: import gevent from gevent import socket, monkey # 打补丁 monkey.patch_all() # 定义单个请求处理逻辑 def handle_request(conn): while True: data, client_addr = conn.recvfrom(1024) # 如果没有新数据,则连接断开了 if not data: conn.close() print("客户端:%s 已断开" % str(client_addr)) else: print('收到数据:%s' % data.decode('gb2312')) conn.send(data) def server(port): s = socket.socket() s.bind(('', port)) # 启动监听,并且设置最大允许并发连接数 s.listen(300) # 监听新客户端连接 while True: conn, addr = s.accept() # 添加协程事件,循环看每个客户端,有一个客户端发生IO,就切到另一个客户端 gevent.spawn(handle_request, conn) if __name__ == '__main__': server(7788)
效果
gevent_socket_tcp_server.py
收到数据:呵呵
收到数据:哈哈
收到数据:呵呵
收到数据:哈哈
收到数据:呵呵
总计花费时间为IO最长的协程
示例代码四(协程并发服务端原生socket):
#!/usr/bin/env python # Author:Zhangmingda import socket,gevent,time from gevent import monkey monkey.patch_all() def socket_server(port): s = socket.socket() s.bind(('0.0.0.0',port)) s.listen(300) while True: cli_obj,addr = s.accept() gevent.spawn(handle_request,cli_obj) # 对所有客户端的连接进行监听。每个客户端之间的为并发连接,所有协程IO自动切换 print('客户端已连接',addr) def handle_request(conn): print('服务端开始睡') time.sleep(2) print('睡醒了') while True: data = conn.recv(1024) print('recv:',data) conn.send(data) if not data: conn.shutdown(socket.SHUT_WR) if __name__ == '__main__': socket_server(888)
C:\Users\Administrator\Desktop\Python3_study\venv\Scripts\python.exe C:/Users/Administrator/Desktop/Python3_study/day9/协程做高并发socketserver.py 客户端已连接 ('127.0.0.1', 59787) 客户端已连接 ('127.0.0.1', 59788) 客户端已连接 ('127.0.0.1', 59789) 客户端已连接 ('127.0.0.1', 59790) 客户端已连接 ('127.0.0.1', 59791) 客户端已连接 ('127.0.0.1', 59792) 客户端已连接 ('127.0.0.1', 59793) 客户端已连接 ('127.0.0.1', 59794) 客户端已连接 ('127.0.0.1', 59795) 服务端开始睡 服务端开始睡 服务端开始睡 服务端开始睡 服务端开始睡 服务端开始睡 服务端开始睡 服务端开始睡 服务端开始睡 客户端已连接 ('127.0.0.1', 59796) 服务端开始睡 睡醒了 睡醒了 睡醒了 睡醒了 睡醒了 睡醒了 睡醒了 睡醒了 睡醒了 睡醒了 recv: b'9' recv: b'0' recv: b'2' recv: b'6' recv: b'4' recv: b'5' recv: b'3' recv: b'1' recv: b'8' recv: b'7'
#!/usr/bin/env python # Author:Zhangmingda import socket,threading,time def client(msg): Host = 'localhost' Port = 888 s = socket.socket() s.connect((Host,Port)) msg = str(msg) print('开始睡觉') time.sleep(5) print('醒了') s.sendall(msg.encode('utf8')) data = s.recv(1024) print('Received',data) time.sleep(10) s.close() for i in range(10): t = threading.Thread(target=client,args=(i,)) t.start()
再分析下使用yield实现协程操作例子
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # @Time: 2020/7/5 20:00 # @Author:zhangmingda # @File: yield实现的简易版协程.py # @Software: PyCharm # Description: def consumer(name): print("--->starting eating baozi...") while True: new_baozi = yield print("[%s] is eating baozi %s" % (name, new_baozi)) def producer(): r = con.__next__() r = con2.__next__() n = 0 while n < 5: n += 1 print("\033[32;1m[producer]\033[0m is making baozi %s" % n) con.send(n) con2.send(n) if __name__ == '__main__': con = consumer("c1") con2 = consumer("c2") p = producer()
输出:
--->starting eating baozi...
--->starting eating baozi...
[producer] is making baozi 1
[c1] is eating baozi 1
[c2] is eating baozi 1
[producer] is making baozi 2
[c1] is eating baozi 2
[c2] is eating baozi 2
[producer] is making baozi 3
[c1] is eating baozi 3
[c2] is eating baozi 3
[producer] is making baozi 4
[c1] is eating baozi 4
[c2] is eating baozi 4
[producer] is making baozi 5
[c1] is eating baozi 5
[c2] is eating baozi 5
Process finished with exit code 0
看楼上的例子,我问你这算不算做是协程呢?你说,我他妈哪知道呀,你前面说了一堆废话,但是并没告诉我协程的标准形态呀,我腚眼一想,觉得你说也对,那好,我们先给协程一个标准定义,即符合什么条件就能称之为协程:
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
- 一个协程遇到IO操作自动切换到其它协程
基于上面这4点定义,我们刚才用yield实现的程并不能算是合格的线程,因为它有一点功能没实现,哪一点呢?
posted on 2018-07-31 15:51 zhangmingda 阅读(1344) 评论(0) 编辑 收藏 举报