• 协程


    一.协程

    1.了解协程

    (1)并发:切换 + 保存状态(程序停止等待)

    (2)协程的含义:

      协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的,是组成线程的各个函数

      协程本身没有实体,协程是在单线程的

    (3)为什么要有协程?

      因为想要在单线程内实现并发的效果.

        因为Cpython中有GIL锁,限制了在同一个时间点,只能执行一个线程,所有想要在执行一个线程的期间,充分利用CPU的性能,所以才有了想在单线程内饰并发的效果

    (4)CPU为什么要切换?

      ①因为某个程序阻塞了     ②因为某个程序用完了时间片

      很明显解决①这个问题才能提高效率,所以想要实现单线程的并发,就要解决在单线程内,多个任务函数中,某个任务函数遇见IO操作,马上自动切换到其它任务函数去执行

    2.yield生成器的特点

    yield生成器自带保存状态

    例如:

    def consumer():
    while 1:
    x = yield
    print(x)

    def product():
    g = consumer() #实例化一个对象 调用consumer函数,此时不执行consumer函数
    next(g) #此时执行consumer函数,但是执行到x = yield的时候就停止了,因为没有参数
    for i in range(100): #循环遍历
    g.send(i) #为yield传值 传完值之后,光标就在yield后面停止 等到下次执行的时候,继续先从x执行
    print("这是product函数")

    product() #首先调用product函数

    例如:

    def func():
    sum = 0
    while 1:
    yield sum

    g = func() #当程序执行到这里的时候,并不会执行func函数
    print(next(g)) #遇到第一个next(g)的时候从头到尾执行函数,一直执行到yield,然后光标停在yield后面
    print(next(g)) #第二次执行的时候,就直接从while循环开始执行

    总结:

      (1)yield只能实现单纯的切换函数和保存函数的状态的功能,不能够实现当某一个函数遇到IO操作阻塞时,自动地切换到另一个函数去执行

      (2)变成协程的目标:当某一个函数遇到IO操作阻塞时候,会自动地切换到另一个函数去执行,如果能实现这个功能,那么每一个函数都是一个协程,但是协程的本质还是依靠于yield实现的

      (3)如果只是拿yield去单纯的实现一个切换现象,你就会发现根本没有程序串行执行效率高

    3.greenlet模块

    能简单的实现函数与函数之间的切换,但是遇到IO操作,不能自动切换到其他函数中

      (1)注册一下,函数func,将函数注册成一个对象f

        f = greenlet(func)

      (2)调用func,使用f.switch(),如果func需要传参,就在switch这里传参即可

    当使用switch调用函数func 的时候,什么时候func会停止?

      (1)要么return     (2)要么在func内部又遇到了switch

    例如:

    from greenlet import greenlet

    def eat(name):
    print("%s吃炸鸡" % name) #执行这句话
    f2.switch("牛哞哞") #使用f2.switch调用drink这个函数,并为其传一个名字的参数
    print("%s吃蛋糕" % name)
    f2.switch() #又使用f2.switch调用drink这个函数

    def drink(name):
    print("%s喝啤酒" % name) #执行这句话
    f1.switch() #又使用f1.switch调用eat函数
    print("%s喝可乐" % name)
    f1 = greenlet(eat) #首先注册了两个对象,分别是f1和f2
    f2 = greenlet(drink)
    f1.switch("刘某某") #使用f1.switch调用eat这个函数,并为其传一个名字的参数

    4.gevent模块

    可以实现在某函数内部遇到IO操作阻塞,就会自动的切换到其它函数内部去执行

    g = gevent.spawn(func,参数)

    g.join()   让func函数执行完毕

    gevent.join([g1,g2,g3])   让多个函数执行完毕

    func停止的原因:

      (1)func执行完毕     (2)遇到IO操作的时候

    (1)例如:

    import gevent

    def func():
    print("1 2 3 4")
    gevent.sleep(1) #遇到IO操作,自动执行另一个函数
    print("3 2 3 4")
    gevent.sleep(1) #此时gevent不能识别到其它的IO操作,只能识别自己认识的IO操作(gevent.sleep())
    def func1():
    print("2 2 3 4")
    gevent.sleep(1) #再次遇到IO操作,又回到刚才那个函数去执行
    print("再来一次")
    g1 = gevent.spawn(func) #先注册一个func,将func注册成一个对象g1和g2
    g2 = gevent.spawn(func1)
    g1.join() #通过g1对象执行func函数

    (2)例如:解决gevent不能识别其它的IO操作

    from gevent import monkey
    import gevent
    monkey.patch_all() #可以让gevent识别大部分常用的IO操作
    import time

    def func():
    print("1 2 3 4")
    time.sleep(1)
    print("3 2 3 4")
    def func1():
    print("2 2 3 4")
    time.sleep(1)
    print("再来一次")
    g1 = gevent.spawn(func)
    g2 = gevent.spawn(func1)
    g1.join()
    g2.join()

    (3)例如:串行和并发的效率对比

      并发效率比串行快

    from gevent import monkey
    monkey.patch_all()
    import gevent
    import time

    def func(num):
    time.sleep(1)
    print(num)

    start = time.time()
    for i in range(10): #串行
    func(i)
    print(time.time() - start)


    start = time.time()
    l = []
    for i in range(10):
    g = gevent.spawn(func,i) #协程去并发实现执行任务函数
    l.append(g)
    gevent.joinall(l) #等待l里面的全部函数执行完毕
    print(time.time() - start)

    (4)爬虫实例

    from gevent import monkey
    monkey.patch_all()
    import gevent
    import time
    import requests
    def get_result(url):
    res = requests.get(url)
    print(url,res.status_code,len(res.text))

    url_l = ['http://www.baidu.com',
    'https://www.jd.com',
    'http://www.apache.com',
    'http://www.taobao.com',
    'http://www.qq.com',
    'http://www.mi.com',
    'http://www.cnblogs.com']

    def sync_func(url_l):
    '''同步调用'''
    for url in url_l:
    get_result(url)

    def async_func(url_l):
    '''异步'''
    l = []
    for url in url_l:
    l.append(gevent.spawn(get_result,url))
    gevent.joinall(l)

    start = time.time()
    sync_func(url_l)
    print('sync:',time.time() - start)

    start = time.time()
    async_func(url_l)
    print('async:',time.time() - start)

    5.大的总结   (面试题)

    (1)协程是由用户自己去调度的

    (2)计算密集用多进程,可以充分利用多核CPU的性能

    (3)IO密集用多线程(协程是单线程的)

    (4)多线程和协程的区别:

      ①线程是由操作系统调度,控制

      ②协程是由程序员自己调度,控制

    二.IO多路复用

    1.阻塞IO

    2.非阻塞IO

    3.多路复用IO

    4.异步IO   (python实现不了,但是有tornado框架,天生自带异步)

    例子一:用非阻塞IO模型解决阻塞IO

    服务器端代码:

    import socket
    sk = socket.socket()
    sk.setblocking(False)
    sk.bind(('127.0.0.1',8080))
    sk.listen()

    l = []
    del_l = []
    while 1:
    try:
    conn,addr = sk.accept()# 如果是阻塞IO模型,在这里程序会一直等待。
    l.append(conn)# 将每个请求连接的客户端的conn添加到列表中
    except BlockingIOError:
    for conn in l:# 去遍历所有客户端的conn,看看有没有客户端给我发送数据了

    try:
    info = conn.recv(1024).decode('utf-8')# 尝试接收,看看有没有客户端给我发数据
    if not info:# 如果客户端正常执行了close,服务器会接收到一个空
    del_l.append(conn)# 将已经结束的客户端的conn,添加到要删除的列表中
    print('客户端正常退出了!')
    conn.close()# 因为客户端已经主动close,所以服务器端的conn也要close
    else:
    print(info)
    conn.send(info.upper().encode('utf-8'))
    except BlockingIOError:
    continue# 是没有接受到客户端发来的数据而报错
    except ConnectionResetError:
    pass# 是因为客户端强制退出而报错
    if del_l:
    for conn in del_l:
    l.remove(conn)
    del_l = []# 在删除完主动关闭的客户端的连接之后,应该把此列表清空,否则报错

    客户端代码:

    import socket
    sk = socket.socket()
    sk.connect(('127.0.0.1',8080))

    while 1:
    msg_s = input('>>>')
    if not msg_s:continue
    if msg_s == 'q':break
    sk.send(msg_s.encode('utf-8'))
    print(sk.recv(1024).decode('utf-8'))
    sk.close()

    例子二:基于select的网络IO模型

    服务器端代码:

    import select
    import socket

    sk = socket.socket()
    sk.bind(('127.0.0.1',8080))
    sk.listen()
    del_l = []
    rlist = [sk]# 是用来让select帮忙监听的 所有 接口
    # select:windows/linux是监听事件有没有数据到来
    # poll: linux 也可以做select的工作
    # epoll: linux 也可以做类似的工作
    while 1:
    r,w,x = select.select(rlist,[],[])# 传参给select,当rlist列表中哪个接口有反应,就返回给r这个列表
    if r:
    for i in r:# 循环遍历r,看看有反应的接口到底是sk 还是conn
    if i == sk:
    # 如果是sk,那就表示有客户端的连接请求
    '''sk有数据要接收,代表着有客户端要来连接'''
    conn,addr = i.accept()
    rlist.append(conn)# 把新的客户端的连接,添加到rlist,继续让select帮忙监听
    else:
    # 如果是conn,就表示有客户端给我发数据了
    '''conn有数据要接收,代表要使用recv'''
    try:
    msg_r = i.recv(1024).decode('utf-8')
    if not msg_r:
    '''客户端执行了close,客户端主动正常关闭连接'''
    del_l.append(i)
    i.close()
    else:
    print(msg_r)
    i.send(msg_r.upper().encode('utf-8'))
    except ConnectionResetError:
    pass
    if del_l:# 删除那些主动断开连接的客户端的conn
    for conn in del_l:
    rlist.remove(conn)
    del_l.clear()

    客户端代码:

    import socket
    sk = socket.socket()
    sk.connect(('127.0.0.1',8080))

    while 1:
    msg_s = input('>>>')
    if not msg_s:continue
    if msg_s == 'q':break
    sk.send(msg_s.encode('utf-8'))
    print(sk.recv(1024).decode('utf-8'))
    sk.close()

    面试题

      select 和 poll 和 epoll区别

    (1)select 和 poll 有一个共同的机制,都是采用轮询的方式去询问内核,有没有数据准备好了

    (2)select 有一个最大监听事件的限制,32位机限制1024,6位机限制2048

    (3)poll没有,理论上poll可以开启无限大,1G内存大概够你开10W个事件去监听

    (4)epoll是最好的,采用的是回调机制,解决了select和poll共同存在的问题

        而且epoll理论上也可以开启无限多个监听事件

  • 相关阅读:
    C语言的灵魂(函数)
    GO语言测试题
    gRPC的发布订阅模式
    gRPC 介绍和简单实现
    RPC与Protobuf(五)
    RPC和Protubuf(四)
    RPC与Protobuf(三)
    JS立即执行函数的几种写法
    如何写出让人看了恶心的代码
    记录几个前端必备的库/框架
  • 原文地址:https://www.cnblogs.com/lhy979/p/9549685.html
Copyright © 2020-2023  润新知