11-05 协程

一. 什么是协程?

# 知识储备:
    进程: 资源单位. 多进程下实现并发. 如果多核就是出现并行
    线程: 执行单位. 同一进程下的多线程实现并发.
    
# 协程: (提示: 这个概念完全是程序员自己意淫出来的根本不存在)
	协程就是在单线程下实现并发

二. 为什么要用协程?

# 知识储备: 多道技术.
	多道计数的核心就是切换+保存状态
	切换分2种情况: 
		1) 程序在运行的过程中遇到了IO
		2) 程序的执行时间过长或者有一个优先级更高的程序替代了它
		
# 为什么要用协程? 	
	多道技术可以控制内核级别程序遇到IO或执行时间过长的情况下保存状态以后剥夺程序的CPU执行权限, 进而提升程序的执行效率.
	我们可以在单线程内开启协程, 控制应用程序代码级别遇到IO情况下保存状态以后切换, 以此来提升效率. (提示: 如果是非IO操作的切换与效率无关)
	
# 协程的优点: 应用程序级别速度要远远高于操作系统的切换
	
# 协程的缺点: 多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地, 该线程内的其他的任务都不能执行了.

# 强调!!!: 
	一旦引入协程,就需要检测单线程下所有的IO行为,
    实现遇到IO就切换,少一个都不行,以为一旦一个任务阻塞了,整个线程就阻塞了,
	其他的任务即便是可以计算,但是也无法运行了	

验证: 切换是否就一定提升效率

我们可以基于yield来验证。yield本身就是一种在单线程下可以保存任务运行状态的方法.

# 知识回顾
'''
# 1. yiled可以保存状态,yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级
# 2. send可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换
'''

# 串行执行计算密集型的任务
import time

def func1():
    for i in range(10000000):
        i + 1

def func2():
    for i in range(10000000):
        i + 1

start_time = time.time()
func1()
func2()
print(time.time() - start_time)  # 执行时间: 1.1209993362426758


# 切换 + yield
import time

def func1():
    while True:
        10000000 + 1
        yield

def func2():
    g = func1()  # 先初始化出生成器
    for i in range(10000000):
        i + 1
        next(g)

start_time = time.time()
func2()
print(time.time() - start_time)  # 执行时间: 1.4919734001159668

# 总结由此而知: 如果是非IO操作的切换与效率无关
'''
yield缺陷: yield不能检测IO,实现遇到IO自动切换. 接下来我们使用第三方gevent模块实现
'''

三. 使用第三方gevent模块实现单线程下的协程

1. 安装

# 前提: 安装了环境变量. 这里使用的是清华的源地址, 默认国外的地址下载速度太慢了!!
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple gevent

2. 用法

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

g1 = gevent.spawn(func, 1,, 2, 3, x = 4, y = 5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的. spawn内部调用了g.start()是一种类始于开启进程的异步提交任务的操作.

g2 = gevent.spawn(func2)

g1.join()  # 等待g1结束

g2.join()  # 等待g2结束

# 或者上述两步合作一步:gevent.joinall([g1,g2])

g1.value  # 拿到func1的返回值

3. 协程实现

'''
# spawn /spɔːn/ 再生侠 闪灵悍将 繁衍
# patch /pætʃ/  补丁 修补 修补文件

注意!!!: from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前. 因为gevent在没有打补丁的情况下只能识别gevent自带的IO操作.
'''
from gevent import monkey;monkey.patch_all()
import time
import gevent


"""
gevent模块本身无法检测常见的一些io操作
在使用的时候需要你额外的导入一句话
from gevent import monkey
monkey.patch_all()
又由于上面的两句话在使用gevent模块的时候是肯定要导入的
所以还支持简写
from gevent import monkey;monkey.patch_all()
"""

def heng():
    print('哼')
    time.sleep(2)
    print('哼1')

def ha():
    print('哈')
    time.sleep(3)
    print('哈1')

def heiheihei():
    print('heiheihei')
    time.sleep(5)
    print('heiheihei1')


# 情况1: 单线程默认执行情况下耗时统计
'''
start_time = time.time()
heng()
ha()
heiheihei()
print(time.time() - start_time)  # 10.006284236907959
'''

# 情况2: 单线程使用gevent实现协程遇到IO切换+保存状态耗时统计

# 第一种写法:
'''
start_time = time.time()
g1 = gevent.spawn(heng)  # 内部使用了g.start()
g2 = gevent.spawn(ha)
g3 = gevent.spawn(heiheihei)
g1.join()
g2.join()  # 等待被检测的任务执行完毕 再往后继续执行
g3.join()
print(time.time() - start_time)  # 5.006734848022461
'''


# 第二种写法: 如果是多个spawn就不要直接在后面使用join了, 不然会变成串行执行.而是使用第一种和第二种方式.
'''
start_time = time.time()
gevent.spawn(heng).join()  # 内部使用了g.start()
gevent.spawn(ha).join()
gevent.spawn(heiheihei).join()
print(time.time() - start_time)  # 10.006813526153564
'''


# 第三种写法:  是基于第一种写法的简写
start_time = time.time()
g1 = gevent.spawn(heng)  # 内部使用了g.start()
g2 = gevent.spawn(ha)
g3 = gevent.spawn(heiheihei)
gevent.joinall([g1, g2, g3])
print(time.time() - start_time)  # 5.006734848022461

四. 协程应用: 使用gevent模块实现单线程下的socket并发

通过gevent实现单线程下的socket并发(from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞

1. TCP服务端

from gevent import monkey; monkey.patch_all()
import gevent
from socket import *


'''
如果不想用money.patch_all()打补丁,可以用gevent自带的socket
from gevent import socket
s=socket.socket()
'''

def communication(conn):
    while True:
        try:
            data_bytes = conn.recv(1024)
            if not data_bytes:
                break
            conn.send(data_bytes.upper())
        except ConnectionResetError as e:
            print(e)
            break
    conn.close()

def server_forever(ip, port):  # forever  /fərˈevə(r)/  永远 直到永远 永恒
    server = socket(AF_INET, SOCK_STREAM)
    server.bind((ip, port))
    server.listen(5)
    while True:
        conn, client_address = server.accept()
        # 检测communication中的recv的或者send的IO行为.(主要检测accept)
        gevent.spawn(communication, conn)

if __name__ == '__main__':
    '''
    网络号为127的地址保留用于环回测试本机的进程间通信(127.0.0.0到127.255.255.255是保留地址,用于环回测试,0.0.0.0到0.255.255.255也是保留地址,用于表示所有的IP地址。
    '''
    # 检测server_forever中的accept的IO行为.
    g = gevent.spawn(server_forever, '127.0.0.2', 8080)
    g.join()  # 等待g运行结束. 也就是说一直运行server_forever中的True循环中的代码. 如果这里不指定上面的spawn是异步提交的任务, 整个程序会直接结束.

2. TCP客户端

# 多线程并发多个客户端
from threading import Thread
from threading import current_thread
from socket import *

def client_communication(ip, port):
    client = socket(AF_INET, SOCK_STREAM)
    client.connect((ip, port))
    count = 0
    while True:
        client.send(f'{current_thread().name} say hello!'
                    f' {count}'.encode('utf-8'))
        count += 1
        data_bytes = client.recv(1024)
        print(data_bytes)
    client.close()


if __name__ == '__main__':
    for i in range(100):
        t = Thread(target=client_communication, args=('127.0.0.2', 8080))
        t.start()

五. 总结

理想状态, 我们可以通过:
    多进程下面开设多线程
    多线程下面再开设协程序
    从而使我们的程序执行效率提升
posted @ 2020-04-27 23:55  给你加马桶唱疏通  阅读(165)  评论(0编辑  收藏  举报