Python中协程用法代码详解


Posted in Python onFebruary 10, 2018

本文研究的主要是python中协程的相关问题,具体介绍如下。

Num01?>协程的定义

协程,又称微线程,纤程。英文名Coroutine。

首先我们得知道协程是啥?协程其实可以认为是比线程更小的执行单元。 为啥说他是一个执行单元,因为他自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。

Num02?>协程和线程的差异

那么这个过程看起来和线程差不多。其实不然, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。

Num03?>协程带来的问题

协程有一个问题,就是系统并不感知,所以操作系统不会帮你做切换。 那么谁来帮你做切换?让需要执行的协程更多的获得CPU时间才是问题的关键。

举个例子如下:

目前的协程框架一般都是设计成 1:N 模式。所谓 1:N 就是一个线程作为一个容器里面放置多个协程。 那么谁来适时的切换这些协程?答案是有协程自己主动让出CPU,也就是每个协程池里面有一个调度器, 这个调度器是被动调度的。意思就是他不会主动调度。而且当一个协程发现自己执行不下去了(比如异步等待网络的数据回来,但是当前还没有数据到), 这个时候就可以由这个协程通知调度器,这个时候执行到调度器的代码,调度器根据事先设计好的调度算法找到当前最需要CPU的协程。 切换这个协程的CPU上下文把CPU的运行权交个这个协程,直到这个协程出现执行不下去需要等等的情况,或者它调用主动让出CPU的API之类,触发下一次调度。

Num04?>协程的好处

在IO密集型的程序中由于IO操作远远慢于CPU的操作,所以往往需要CPU去等IO操作。 同步IO下系统需要切换线程,让操作系统可以在IO过程中执行其他的东西。 这样虽然代码是符合人类的思维习惯但是由于大量的线程切换带来了大量的性能的浪费,尤其是IO密集型的程序。

所以人们发明了异步IO。就是当数据到达的时候触发我的回调。来减少线程切换带来性能损失。 但是这样的坏处也是很大的,主要的坏处就是操作被 “分片” 了,代码写的不是 “一气呵成” 这种。 而是每次来段数据就要判断 数据够不够处理哇,够处理就处理吧,不够处理就在等等吧。这样代码的可读性很低,其实也不符合人类的习惯。

但是协程可以很好解决这个问题。比如 把一个IO操作 写成一个协程。当触发IO操作的时候就自动让出CPU给其他协程。要知道协程的切换很轻的。 协程通过这种对异步IO的封装 既保留了性能也保证了代码的容易编写和可读性。在高IO密集型的程序下很好。但是高CPU密集型的程序下没啥好处。

Num05?>yield实现一个简单协程案例

import time
def A():
  while True:
    print("----我是A函数---")
    yield
    time.sleep(0.5)
def B(c):
  while True:
    print("----我是B函数---")
    next(c)
    time.sleep(0.5)
if __name__ == '__main__':
  a = A()
  B(a)

# 结果如下:
# ----我是B函数---
# ----我是A函数---
# ----我是B函数---
# ----我是A函数---
# ----我是B函数---
# ----我是A函数---
# ----我是B函数---
# ----我是A函数---
# ----我是B函数---
# ----我是A函数---
# ......

Num06?>greenlet版本实现协程案例

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : xiaoke
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)


def main():
  # 切换到gr1中运行
  gr1.switch()


if __name__ == '__main__':
  gr1 = greenlet(test1)
  gr2 = greenlet(test2)
  main()

# 结果如下:
# ---我是A函数--
# ---我是B函数--
# ---我是A函数--
# ---我是B函数--
# ---我是A函数--
# ---我是B函数--
# ---我是A函数--
# ---我是B函数--
# ......

Num07?>gevent实现协程案例

原理:其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。

由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : xiaoke
import gevent


def task1(n):
  for i in range(n):
    print('----task1-----')

    gevent.sleep(1)
    # time.sleep(1) # time.sleep没有让gevent感知到等待


def task2(n):
  for i in range(n):
    print('----task2-----')

    gevent.sleep(1)
    # time.sleep(1)


def main():
  g1 = gevent.spawn(task1, 5)
  g2 = gevent.spawn(task2, 5)

  g1.join()
  g2.join()


if __name__ == "__main__":
  main()


# 结果如下:
# ----task1-----
# ----task2-----
# ----task1-----
# ----task2-----
# ----task1-----
# ----task2-----
# ----task1-----
# ----task2-----
# ----task1-----
# ----task2-----

gevent并发下载器

实际代码里,我们不会用gevent.sleep()去切换协程,而是在执行到IO操作时,gevent自动切换,代码如下

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : xiaoke

import urllib.request # py3

import gevent
from gevent import monkey
# 猴子补丁,将标准库的涉及IO操作方法替换成gevent
monkey.patch_all() 

# 协程的任务函数
def my_download(url):
  print('GET %s' % url)

  response = urllib.request.urlopen(url)
  data = response.read()

  print('下载 %d bytes from %s' % (len(data), url))


def main():
  g1 = gevent.spawn(my_download, 'http://www.google.cn')
  g2 = gevent.spawn(my_download, 'http://www.qq.com')
  g3 = gevent.spawn(my_download, 'http://www.baidu.com')

  gevent.joinall([g1, g2, g3]) # 等待指定的协程结束


if __name__ == "__main__":
  main()

# 结果如下:
# GET http://www.google.cn
# GET http://www.qq.com
# GET http://www.baidu.com
# 下载 102221 bytes from http://www.baidu.com
# 下载 52297 bytes from http://www.qq.com
# 下载 3213 bytes from http://www.google.cn

#从上能够看到是先获取baidu的相关信息,然后依次是qq
#google,但是收到数据的先后顺序不一定与发送顺序相同,
#这也就体现出了异步,即不确定什么时候会收到数据,顺序不一定.

Num08?>gevent版?TCP服务器

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : xiaoke
import socket
import gevent
from gevent import monkey
#猴子补丁,将标准库的涉及IO操作方法替换成gevent
monkey.patch_all()


# 需要为客户端提供服务
def do_service(connect_socket):
  while True:
    # tcp recv() 只会返回接收到的数据
    recv_data = connect_socket.recv(1024)

    # if recv_data == b'':
    if len(recv_data) == 0:
      # 发送方关闭tcp的连接,recv()不会阻塞,而是直接返回''
      # print('client %s close' % str(client_addr))
      # s.getpeername()  s.getsockname()
      print('client %s close' % str(connect_socket.getpeername()))
      break
    print('recv: %s' % recv_data.decode('gbk'))

def main():

  listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  # 设置允许复用地址,当建立连接之后服务器先关闭,设置地址复用
  listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

  my_addr = ('192.168.105.125', 8080)
  listen_socket.bind(my_addr)

  listen_socket.listen(5) # 设置套接字成监听,5表示一个己连接队列长度
  print('listening...')

  while True:
    # 接受连接请求,创建连接套接字,用于客户端连通信
    connect_socket, client_addr = listen_socket.accept() # accept默认会引起阻塞
    # 新创建连接用的socket, 客户端的地址
    # print(connect_socket)
    print(client_addr)

    # 每当来新的客户端连接,创建协程,由协程和客户端通信
    coroutine_do_service = gevent.spawn(do_service, connect_socket)


if __name__ == "__main__":
  main()

总结

关于协程的问题,面试中好像也会时常被问到,大家一定要注意理解,概念,怎么去实现。

以上就是本文关于Python中协程用法代码详解的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

Python 相关文章推荐
python的迭代器与生成器实例详解
Jul 16 Python
Python中不同进制互相转换(二进制、八进制、十进制和十六进制)
Apr 05 Python
简单介绍Python2.x版本中的cmp()方法的使用
May 20 Python
Python实现Dijkstra算法
Oct 17 Python
在Pycharm中项目解释器与环境变量的设置方法
Oct 29 Python
Python字符串的一些操作方法总结
Jun 10 Python
如何基于python3和Vue实现AES数据加密
Mar 27 Python
python numpy实现rolling滚动案例
Jun 08 Python
浅谈pytorch中torch.max和F.softmax函数的维度解释
Jun 28 Python
python爬虫scrapy基本使用超详细教程
Feb 20 Python
python单元测试之pytest的使用
Jun 07 Python
python异常中else的实例用法
Jun 15 Python
Python实现简单生成验证码功能【基于random模块】
Feb 10 #Python
Django中Forms的使用代码解析
Feb 10 #Python
Python中列表与元组的乘法操作示例
Feb 10 #Python
Python程序运行原理图文解析
Feb 10 #Python
Python迭代器和生成器定义与用法示例
Feb 10 #Python
Python中装饰器学习总结
Feb 10 #Python
Python基于hashlib模块的文件MD5一致性加密验证示例
Feb 10 #Python
You might like
php去除HTML标签实例
2013/11/06 PHP
php加密算法之实现可逆加密算法和解密分享
2014/01/21 PHP
分享php邮件管理器源码
2016/01/06 PHP
9个比较实用的php代码片段
2016/03/15 PHP
laravel通过创建自定义artisan make命令来新建类文件详解
2017/08/17 PHP
PHP ob缓存以及ob函数原理实例解析
2020/11/13 PHP
用javascript实现的仿Flash广告图片轮换效果
2007/04/24 Javascript
javascript-简单的日历实现及Date对象语法介绍(附图)
2013/05/30 Javascript
详解vue.js+UEditor集成 [前后端分离项目]
2017/07/07 Javascript
详解jQuery中关于Ajax的几个常用的函数
2017/07/17 jQuery
微信小程序switch组件使用详解
2018/01/31 Javascript
js实现控制文件拖拽并获取拖拽内容功能
2018/02/17 Javascript
原生js实现拖拽功能基本思路详解
2018/04/18 Javascript
JointJS流程图的绘制方法
2018/12/03 Javascript
小程序多图列表实现性能优化的方法步骤
2019/05/28 Javascript
Jquery动态列功能完整实例
2019/08/30 jQuery
解决vue cli使用typescript后打包巨慢的问题
2019/09/30 Javascript
新手入门js闭包学习过程解析
2019/10/08 Javascript
通过实例解析js可枚举属性与不可枚举属性
2020/12/02 Javascript
ES6 十大特性简介
2020/12/09 Javascript
JS addEventListener()和attachEvent()方法实现注册事件
2021/01/11 Javascript
Python脚本简单实现打开默认浏览器登录人人和打开QQ的方法
2016/04/12 Python
Python中内建函数的简单用法说明
2016/05/05 Python
python中set()函数简介及实例解析
2018/01/09 Python
python爬取拉勾网职位数据的方法
2018/01/24 Python
Python WSGI的深入理解
2018/08/01 Python
python提取包含关键字的整行数据方法
2018/12/11 Python
Python获取时间范围内日期列表和周列表的函数
2019/08/05 Python
Python Django 简单分页的实现代码解析
2019/08/21 Python
html5 canvas实现给图片添加平铺水印
2019/08/20 HTML / CSS
Revolution Beauty美国官网:英国知名化妆品网站
2018/07/23 全球购物
社区爱国卫生月活动总结
2014/06/30 职场文书
应届生自荐信
2014/06/30 职场文书
计算机实训报告范文
2014/11/05 职场文书
收入证明范本
2015/06/12 职场文书
一篇文章学会Vue中间件管道
2021/06/20 Vue.js