Pyhton_day17--协程

一、异步IO

1、为什么要使用异步IO

在IO编程一节中,我们已经知道,CPU的速度远远快于磁盘、网络等IO。在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。这种情况称为同步IO。
在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。
因为一个IO操作就阻塞了当前线程,导致其他代码无法执行,所以我们必须使用多线程或者多进程来并发执行代码,为多个用户服务。每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程不受影响。

多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降。由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法。

2、什么时候使用异步IO

解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。

3、异步IO的工作状态

消息模型其实早在应用在桌面应用程序中了。一个GUI程序的主线程就负责不停地读取消息并处理消息。所有的键盘、鼠标等消息都被发送到GUI程序的消息队列中,然后由GUI程序的主线程处理。由于GUI线程处理键盘、鼠标等消息的速度非常快,所以用户感觉不到延迟。某些时候,GUI线程在一个消息处理的过程中遇到问题导致一次消息处理时间过长,此时,用户会感觉到整个GUI程序停止响应了,敲键盘、点鼠标都没有反应。这种情况说明在消息模型中,处理一个消息必须非常迅速,否则,主线程将无法及时处理消息队列中的其他消息,导致程序看上去停止响应。消息模型是如何解决同步IO必须等待IO操作这一问题的呢?当遇到IO操作时,代码只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程。当IO操作完成后,将收到一条“IO完成”的消息,处理该消息时就可以直接获取IO操作结果。在“发出IO请求”到收到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。

二、什么是协程

协程,又称微线程,纤程。英文名Coroutine。协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛
应用。子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

三、协程理解

1、例如:

def A():
    print('1')
    print('2')
    print('3')


def B():
    print('x')
    print('y')
    print('z')

看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行


2、协程的优点

最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。


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

3、协程的实现方式

例:yield关键字

def chat_robot():
    res = ''
    while True:
        receive = yield
        if 'age' in receive:
            res = "18"
        elif 'name' in receive:
            res = "小冰"
        elif 'hello' in receive:
            res = 'hello'
        else:
            res = "i dont know"
        print("Robot>>:%s" %(res))

def main():
    # Robot
    Robot = chat_robot()
    next(Robot)
    while True:
        send_data = input("A>>:")
        if send_data == 'q' or send_data == 'bye':
            print("不聊了")
            break
        Robot.send(send_data)


main()
  1. 带有 yield 的函数不再是一个普通函数,而是一个生成器generator,可用于迭代,工作原理同上。
  2. yield 是一个类似 return 的关键字,迭代一次遇到yield时就返回yield后面的值。重点是:下一次迭代时,从上一次迭代遇到的yield后面的代码开始执行。
  3. 简要理解:yield就是 return 返回一个值,并且记住这个返回的位置,下次迭代就从这个位置后开始。
  4. 带有yield的函数不仅仅只用于for循环中,而且可用于某个函数的参数,只要这个函数的参数允许迭代参数。比如array.extend函数,它的原型是array.extend(iterable)。
  5. send(msg)与next()的区别在于send可以传递参数给yield表达式,这时传递的参数会作为yield表达式的值,而yield的参数是返回给调用者的值。——换句话说,就是send可以强行修改上一个yield表达式值。比如函数中有一个yield赋值,a
    = yield 5,第一次迭代到这里会返回5,a还没有赋值。第二次迭代时,使用.send(10),那么,就是强行修改yield 5表达式的值为10,本来是5的,那么a=10
  6. send(msg)与next()都有返回值,它们的返回值是当前迭代遇到yield时,yield后面表达式的值,其实就是当前迭代中yield后面的参数。
  7. 第一次调用时必须先next()或send(None),否则会报错,send后之所以为None是因为这时候没有上一个yield(根据第8条)。可以认为,next()等同于send(None)。
  8. Python通过yield提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。

四、python中的协程---gevent

gevent是第三方库,通过greenlet实现协程,其基本思想是:

当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。

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

例:

from gevent import monkey
import threading
# 打补丁, 自动修改协程中需要的一些标准库;
monkey.patch_socket()
import gevent

def f(n):
    """协程需要处理的任务"""
    for i in range(n):
        print(gevent.getcurrent().name,threading.current_thread().name,  i)
        # 模拟IO操作
        # 通过gevent.sleep交出协程的控制权;
        gevent.sleep(1)
        
def Automain():
    # g1 = gevent.spawn(f, 5)
    # g2 = gevent.spawn(f, 5)
    # g3 = gevent.spawn(f, 5)
    #
    # g1.join()
    # g2.join()
    # g3.join()
    gevents = [gevent.spawn(f, 5) for i in range(3)]
    gevent.joinall(gevents)

Automain()

threading在这里主要是我们看到运行的结果对比,证明我们这里的协程是单线程;

Automain()实例三个对像

第一列为实例返回回来的对象,第二列为我们的线程只有一条为主线程;

1、协程与线程池的比较

注:我这里用了时间装饰器来观察实验结果,

from concurrent.futures import ThreadPoolExecutor
from urllib.request import urlopen

import gevent
from gevent import monkey

from mytimeit import timeit

monkey.patch_socket()

urls = ['http://httpbin.org']*20

def load_url(url):
    with urlopen(url) as urlobj:
        content = urlobj.read().decode('utf-8')

    print('%s 有 %s 字节'%(url,len(content)))

@timeit
def geventMain():
    jobs = [gevent.spawn(load_url,url) for url in urls ]
    gevent.joinall(jobs)

@timeit
def threadMain():
    with ThreadPoolExecutor(max_workers=100) as pool:
        pool.map(load_url, urls)

geventMain()
threadMain()

这里的实验结果不一定谁更快,因为我的实验数据太少了,如果需要操作更大的数据,那么结果会更加明显;

五、利用协程爬取贴吧邮箱

下面的代码里需要用到正则表达的内容,我们在后面的内容中会讲到;

from concurrent.futures import ThreadPoolExecutor
from urllib import request
import re
from gevent import monkey
import threading
# 打补丁, 自动修改协程中需要的一些标准库;
monkey.patch_socket()
import gevent

from mytime import timeit

url = 'http://tieba.baidu.com/p/2314539885'
EmailLi = []

def get_content(url):
    """获取网页源代码"""
    # 1. 下载网页源代码到本地, 获取帖子总页数;
    # urlObj = request.urlopen(url, timeout=60)
    # content = urlObj.read().decode('utf-8')
    with request.urlopen(url, timeout=60) as urlObj:
        content = urlObj.read().decode('utf-8')
        return content


def get_page(url):
    content = get_content(url)
    # <a href="/p/2314539885?pn=31">尾页</a>
    pattern = r'<a href="/p/.*pn=(\d+)">尾页</a>'
    page = re.findall(pattern, content)[0]
    return int(page)


def get_all_url(url, page):
    """生成所有页的帖子url地址"""
    url_li = []
    # page: 31  0,1,2,3,4.....30

    for i in range(page):
        new_url = url + '?pn=%d' %(i+1)
        url_li.append(new_url)
    return url_li


def get_email(url):
    content = get_content(url)
    pattern = r'[a-zA-Z0-9_]+@\w+\.com'
    EmailLi.extend(re.findall(pattern, content))
    print(re.findall(pattern, content))

# 结论:
#   1.  如果代码是IO密集型, 建议选择多线程;
#   2.  如果是计算密集型, 建议选用多进程;

@timeit
def geventMain():
    # url = 'http://tieba.baidu.com/p/2314539885'
    page = get_page(url)
    url_li = get_all_url(url, page)
    gevents = [gevent.spawn(get_email, url) for url in url_li]
    gevent.joinall(gevents)
geventMain()

也可以用线程池进行比较

@timeit
def useNoThreadMain():
    page = get_page(url)
    url_li = get_all_url(url, page)
    for urlItem in url_li:
        print(get_email(urlItem))








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值