协程介绍及基本示例

协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程

  协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:

协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

  协程的好处:

  • 无需线程上下文切换的开销
  • 无需原子操作锁定及同步的开销
    • "原子操作(atomic operation)是不需要synchronized",所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。
  • 方便切换控制流,简化编程模型
  • 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

  缺点:

  • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  • 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序。

1、yield实现协程

import time

def consumer(name):
print("--->starting eating baozi...")
while True:
new_baozi = yield # yield设置生成器
print("[{0}] is eating baozi {1}".format(name, new_baozi)) def producer():
r = con.__next__() # 调用生成器
r = con2.__next__()
n = 0
while n < 5:
n += 1
con.send(n) # 唤醒生成器,并且向生成器传值
con2.send(n)
time.sleep(1)
print("\033[32m[producer]\033[0m is making baozi {0}".format(n)) if __name__ == '__main__':
con = consumer("c1") # 创建一个生成器c1
con2 = consumer("c2") # 创建一个生产器C2
p = producer()

1、send有两个作用?

  ①唤醒生产器 ②给yield传一个值,就是yield接收到的这个值。这个说明yield在被唤醒的时候可以接收数据。

2、怎么实现我们的单线程实现并发的效果呢?

  遇到IO操作就切换,IO比较耗时,协程之所以能处理大并发,就是IO操作会挤掉大量的时间。没有IO操作的话,整个程序只有cpu在运算了,因为cpu很快,所以你感觉是在并发执行的。

3、IO操作完成了,程序什么时候切回去?

  IO操作一旦完成,我们就自动切回去。

4、IO是什么?

Python中的io模块是用来处理各种类型的I/O操作流。主要有三种类型的I/O类型:文本I/O(Text I/O),二进制I/O(Binary I/O)和原始I/O(Raw I/O)。它们都是通用类别,每一种都有不同的后备存储。属于这些类别中的任何一个的具体对象称为文件对象,其他常用的术语为流或者类文件对象。

  除了它的类别,每一种具体的流对象也具有各种功能:它仅仅允许读,或者仅仅允许写,或者既能读又能写。它也允许任意随机访问(向前或者向后寻找任何位置),或者仅仅顺序访问(例如在套接字或管道中)。

  所有的流对于提供给它们的数据的数据类型都很严格。例如,如果用一个二进制流的write()方法写一个字符类型的数据,那么将会触发一个TypeError错误。用文本流的write()方法来写字节对象数据也是一样的,会触发该错误。

二、手动实现切换IO

Greenlet是python的一个C扩展,来源于Stackless python,旨在提供可自行调度的‘微线程’, 即协程。它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator

from greenlet import greenlet

def test1():
print(12)
gr2.switch() # 切换到test2
print(34)
gr2.switch() # 切换到test2 def test2():
print(56)
gr1.switch() # 切换到test1
print(78) gr1 = greenlet(test1) # 启动一个协程
gr2 = greenlet(test2)
gr1.switch() # 切换到test1,这个switch不写的话,会无法输出打印 #执行结果
12
56
34
78

小结:

  1. cpu值认识线程,而不认识协程,协程是用户自己控制的,cpu根本都不知道它们的存在。
  2. 线程的上下文切换保存在cpu的寄存器中,但是协程拥有自己的寄存上下文和栈。
  3. 协程是串行的,无需锁。

虽然greenlet确实用着比generator(生成器)还简单了,但好像还没有解决一个问题,就是遇到IO操作,自动切换,对不对?

三、协程遇IO操作自动切换

下来就说说如何遇到IO就自动切换切换,Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

import gevent

def foo():
print("Running in foo")
gevent.sleep(3) # 模仿io操作,一遇到io操作就切换
print("Explicit context switch to foo again") def bar():
print("Explicit context to bar")
gevent.sleep(1)
print("Implicit context switch back to bar") def fun3():
print("running fun3")
gevent.sleep(0) # 虽然是0秒,但是会触发一次切换
print("running fun3 again") gevent.joinall([
gevent.spawn(foo), # 生成协程
gevent.spawn(bar),
gevent.spawn(fun3)
]) #执行结果
Running in foo
Explicit context to bar
running fun3
running fun3 again
Implicit context switch back to bar
Explicit context switch to foo again

当foo遇到sleep(2)的时候,切自动切换到bar函数,执行遇到sleep(1)的时候自动切换到fun3函数,遇到sleep(0)又自动切换到foo。这个时候sleep(2)还没有执行完毕,又切换到bar的sleep(1)这边,发现又没有执行完毕,就有执行fun3这边,发现sleep(0)执行完毕,则继续执行,然后又切换到foo,发现sleep(2)又没有执行完毕,就切换到bar的sleep(1)这边,发现执行完了,有切回到foo这边,执行完毕。

主要作用:比如说你现在又50处IO,然后总共加起来串行的的话,要花100秒,但是50处IO最长的那个IO只花了5秒钟,那代表中你的这个程序就是协程最多5秒就执行完毕了。

符合下面四个条件才能称之为协程:

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁
  3. 用户程序里自己保存多个控制流的上下文栈
  4. 一个协程遇到IO操作自动切换到其它协程

协程(gevent)并发爬网页

上面例子gevent遇到io自动切换,现在就来实际演示协程爬虫的例子

1、正常(串行)爬网页

串行效果的爬网页的代码,看看消耗多长时间

from urllib import request
import time def run(url):
print("GET:{0}".format(url))
resp = request.urlopen(url) # request.urlopen()函数 用来打开网页
data = resp.read() # 读取爬到的数据
with open("url.html", "wb") as f:
f.write(data)
print('{0} bytes received from {1}'.format(len(data), url)) urls = [
'http://www.163.com/',
'https://www.yahoo.com/',
'https://github.com/'
] time_start = time.time() # 开始时间
for url in urls:
run(url)
print("同步cost", time.time() - time_start) # 程序执行消耗的时间 #执行结果
GET:http://www.163.com/
659094 bytes received from http://www.163.com/
GET:https://www.yahoo.com/
505819 bytes received from https://www.yahoo.com/
GET:https://github.com/
56006 bytes received from https://github.com/
同步cost 4.978517532348633

  

2、协程(gevent)爬虫

用gevent并发执行一下,看看效果。

from urllib import request
import gevent,time
def run(url):
print("GET:{0}".format(url))
resp = request.urlopen(url) # request.urlopen()函数 用来打开网页
data = resp.read() # 读取爬到的数据
with open("url.html", "wb") as f:
f.write(data)
print('{0} bytes received from {1}'.format(len(data), url)) urls = [
'http://www.163.com/',
'https://www.yahoo.com/',
'https://github.com/'
] time_start = time.time() # 开始时间
gevent.joinall([ # 用gevent启动协程
gevent.spawn(run, 'http://www.163.com/'), # 第二个值是传入参数,之前我们没有讲,因为前面没有传参
gevent.spawn(run, 'https://www.yahoo.com/'),
gevent.spawn(run, 'https://github.com/'),
])
print("同步cost", time.time() - time_start) # 程序执行消耗的时间 #执行结果
GET:http://www.163.com/
659097 bytes received from http://www.163.com/
GET:https://www.yahoo.com/
503844 bytes received from https://www.yahoo.com/
GET:https://github.com/
55998 bytes received from https://github.com/
同步cost 4.433035850524902

对比1、2爬网页的例子,发现执行耗费时间上并没有得到明显提升,并没有并发爬网页的神奇快感,其实主要是因为gevent现在检测不到urllib的IO操作。它都不知道urllib进行了IO操作,感受不到阻塞,它都不会进行切换,所以它就串行了。

3、打个补丁,告诉gevent,urllib正在进行IO操作

通过导入monkey模块,来打这个补丁,原代码不变,就添加一行monkey.patch_all()即可。

from urllib import request
import gevent,time
from gevent import monkey # 导入monkey模块 monkey.patch_all() # 把当前程序的所有的IO操作给作上标记 def run(url):
print("GET:{0}".format(url))
resp = request.urlopen(url) # request.urlopen()函数 用来打开网页
data = resp.read() # 读取爬到的数据
with open("url.html", "wb") as f:
f.write(data)
print('{0} bytes received from {1}'.format(len(data), url)) urls = [
'http://www.163.com/',
'https://www.yahoo.com/',
'https://github.com/'
] time_start = time.time() # 开始时间
gevent.joinall([ # 用gevent启动协程
gevent.spawn(run, 'http://www.163.com/'), # 第二个值是传入参数,之前我们没有讲,因为前面没有传参
gevent.spawn(run, 'https://www.yahoo.com/'),
gevent.spawn(run, 'https://github.com/'),
])
print("同步cost", time.time() - time_start) # 程序执行消耗的时间 #执行结果
GET:http://www.163.com/
GET:https://www.yahoo.com/
GET:https://github.com/
659097 bytes received from http://www.163.com/
503846 bytes received from https://www.yahoo.com/
55998 bytes received from https://github.com/
同步cost 1.8789663314819336

原本将近5秒的耗时现在只用了不到2秒就完成,这就是协程的魅力,通过打补丁来检测urllib,它就把urllib里面所有涉及到的有可能进行IO操作的地方直接花在前面加一个标记,这个标记就相当于gevent.sleep(),所以把urllib变成一个一有阻塞,它就切换了

4、gevent实现单线程下的多socket并发

4.1、server端

import sys,gevent,socket,time
from gevent import socket,monkey
monkey.patch_all() def server(port):
s = socket.socket()
s.bind(('0.0.0.0', port))
s.listen(500)
while True:
cli, addr = s.accept()
gevent.spawn(handle_request, cli) #协程 def handle_request(conn):
try:
while True:
data = conn.recv(1024)
print("recv:", data)
conn.send(data)
if not data:
conn.shutdown(socket.SHUT_WR)
except Exception as ex:
print(ex)
finally:
conn.close()
if __name__ == '__main__':
server(8888)

  

4.2、client端

import socket

HOST = 'localhost'    # The remote host
PORT = 8888 # The same port as used by the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
msg = bytes(input(">>:"),encoding="utf8")
s.sendall(msg)
data = s.recv(1024)
print('Received', repr(data))
s.close()

  

【python】-- 协程介绍及基本示例、协程遇到IO操作自动切换、协程(gevent)并发爬网页的更多相关文章

  1. 突破python缺陷,实现几种自定义线程池 以及进程、线程、协程的介绍

    Python线程 Threading用于提供线程相关的操作,线程是应用程序中工作的最小单元. #!/usr/bin/env python # -*- coding:utf-8 -*- import t ...

  2. python 全栈开发,Day43(引子,协程介绍,Greenlet模块,Gevent模块,Gevent之同步与异步)

    昨日内容回顾 I/O模型,面试会问到I/O操作,不占用CPU.它内部有一个专门的处理I/O模块.print和写log 属于I/O操作,它不占用CPU 线程GIL保证一个进程中的多个线程在同一时刻只有一 ...

  3. {python之协程}一 引子 二 协程介绍 三 Greenlet 四 Gevent介绍 五 Gevent之同步与异步 六 Gevent之应用举例一 七 Gevent之应用举例二

    python之协程 阅读目录 一 引子 二 协程介绍 三 Greenlet 四 Gevent介绍 五 Gevent之同步与异步 六 Gevent之应用举例一 七 Gevent之应用举例二 一 引子 本 ...

  4. python全栈开发,Day43(引子,协程介绍,Greenlet模块,Gevent模块,Gevent之同步与异步)

    昨日内容回顾 I/O模型,面试会问道 I/O操作,不占用CPU,它内部有一个专门的处理I/O模块 print和写log属于I/O操作,它不占用CPU 线程 GIL保证一个进程中的多个线程在同一时刻只有 ...

  5. python 并发编程 协程 协程介绍

    协程:是单线程下的并发,又称微线程,纤程.英文名Coroutine.一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的 需要强调的是: 1. python的线程属于内 ...

  6. python之协程与IO操作

    协程 协程,又称微线程,纤程.英文名Coroutine. 协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用. 子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B ...

  7. 协程介绍, Greenlet模块,Gevent模块,Genvent之同步与异步

    昨日内容回顾 I/O模型,面试会问到I/O操作,不占用CPU.它内部有一个专门的处理I/O模块.print和写log 属于I/O操作,它不占用CPU 线程GIL保证一个进程中的多个线程在同一时刻只有一 ...

  8. 协程-遇到I/O自动切换

    参考博客:http://www.cnblogs.com/alex3714/articles/5248247.html 一.前言 Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步 ...

  9. Python 协程并发爬虫网页

    简单爬虫实例: 功能:通过urllib.request实现网站爬虫,捕获网站内容. from urllib import request def f(url): print("GET:%s& ...

随机推荐

  1. css 文字超出省略号

    white-space:nowrap; overflow:hidden; -o-text-overflow:ellipsis; text-overflow:ellipsis; 语法: text-ove ...

  2. linux的chown命令

    chown将指定文件的拥有者改为指定的用户或组,用户可以是用户名或者用户ID:组可以是组名或者组ID:文件是以空格分开的要改变权限的文件列表,支持通配符.系统管理员经常使用chown命令,在将文件拷贝 ...

  3. HDU 2767 Proving Equivalences (强联通)

    pid=2767">http://acm.hdu.edu.cn/showproblem.php?pid=2767 Proving Equivalences Time Limit: 40 ...

  4. JMS 在 SpringBoot 中的使用

    当前环境 Mac OS 10.11.x docker 1.12.1 JDK 1.8 SpringBoot 1.5 前言 基于之前一篇“一个故事告诉你什么是消息队列”,了解了消息队列的使用场景以及相关的 ...

  5. Beautiful Soup 4.4.0 基本使用方法

    Beautiful Soup 4.4.0 基本使用方法Beautiful Soup 安装 pip install  beautifulsoup4 标准库有html.parser解析器但速度不是很快一般 ...

  6. How to use Variables in different component

    1. In Script Task component Set Value: Dts.Variables["ErrorMsg"].Value = string.Format(&qu ...

  7. Android学习(二十二)ContentMenu上下文菜单

    一.上下问菜单 在某个菜单项上长按,会弹出一个菜单,这个就是上下文菜单.有点类似与Windows系统中的右键菜单. 二.上下文菜单的内容 1.标题 2.图标 3.菜单项 4.对应的菜单事件 三.Opt ...

  8. jmap命令(Java Memory Map)的使用

    jmap的使用能够參考: 官方文档 http://docs.oracle.com/javase/6/docs/technotes/tools/share/jmap.html 和这篇博客 http:// ...

  9. Codeforces Round #254 (Div. 2) B (445B)DZY Loves Chemistry

    推理可得终于结果为2的(n-可分组合数)次方. 问题是怎么求出可分组合数,深搜就可以,当然并查集也能够. AC代码例如以下: 深搜代码!!! #include<iostream> #inc ...

  10. 51单片机 | 使用D/A转换器实现三角波发生器

    ———————————————————————————————————————————— D/A转换器 CS=0.ILE=1时,WR1信号有效时将数据总线上的信号写入8位输入锁存器 XFER=0时,W ...