040.并发编程之协程

040.并发编程之协程

文章目录

  • 一、并发编程之线程
    • (一)线程池和进程池的shutdown
    • (二)定时器了解
  • 二、并发编程之携程
    • (一)协程介绍
      • (1)单核下并发的本质及切换任务的两种情况
        • 1.切换任务的情况一:
        • 2.切换任务的情况二:
        • 3.代码验证:yield与send实现程序间切换的
      • (2)协程(Coroutine)
        • 1.什么是协程
        • 2.python下的线程及协程
        • 3.协程优缺点
    • (二)greenlet模块
    • (三)gevent模块+猴子补丁的使用
      • (1)Gevent是什么
      • (2)代码示例
    • (四)asyncio模块了解
      • (1)python3.5以前写法:
      • (2)python3.8的写法
    • (五)必须会的I/O操作与模型
      • (1)IO操作的本质
      • (2)IO模型
        • 1.BIO—阻塞模式I/O
        • 2.NIO—非阻塞模式I/O
        • 3.IO Multiplexing—I/O多路复用模型
        • 4.AIO—异步I/O模型
        • 5.select poll和epoll
          • ①select, poll 和 epoll都是io多路复用技术
          • ②select
          • ③poll
          • ④epoll
          • ⑤更好的例子理解
          • ⑥总结
  • 三、补充:虚拟环境相关知识

一、并发编程之线程

(一)线程池和进程池的shutdown

# 主线程等待所有任务完成
from concurrent.futures import ThreadPoolExecutor
import time
pool = ThreadPoolExecutor(3)
def task(name):
    print('%s 开始' % name)
    time.sleep(1)
    print('%s 结束' % name)
if __name__ == '__main__':
    for i in range(20):
        pool.submit(task, '沙雕%s' % i)
    pool.shutdown(wait=True)  # 放到for循环外面,等待所有任务完成,并且把池关闭
    # pool.submit(task, 'qqqqq')  # 关闭进程池之后不能再提交任务了 RuntimeError: cannot schedule new futures after shutdown
    print('>>>main<<<')

(二)定时器了解

# 控制多长时间后执行一个任务
from threading import Timer
def task(name):
    print('汤姆大战杰瑞————%s' % name)
if __name__ == '__main__':
    # t = Timer(2, task, args=('lxx',))  # 本质是开一个线程,延迟两秒执行
    t = Timer(2, task, kwargs={'name': 'lxx'})  # 本质是开一个线程,延迟两秒执行
    t.start()

二、并发编程之携程

(一)协程介绍

(1)单核下并发的本质及切换任务的两种情况

​ 切换的本质是:切换+保存状态

1.切换任务的情况一:

​ 任务发生阻塞。(可以提升效率,因为充分利用I/O阻塞的时间。)

2.切换任务的情况二:

​ 占用CPU时间过长,或有一个优先级更高的程序替代了它。(不能提升效率,实现“同时”执行的效果。)

3.代码验证:yield与send实现程序间切换的

yield可以保存状态,yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级。

send可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换。

# 代码验证:单纯的切换反而会降低运行效率
# 串行执行
import time
def func1():
    for i in range(100000000):
        i += 1
def func2():
    for i in range(100000000):
        i += 1
if __name__ == '__main__':
    ctime = time.time()
    func1()
    func2()
    print(time.time() - ctime)  # 10.576383590698242
    
# 单纯的切换
import time
def func1():
    for i in range(100000000):
        i += 1
        yield
def func2():
    g = func1()  # 先执行一下func1
    for i in range(100000000):
        i += 1
        next(g)  # 回到func1执行
if __name__ == '__main__':
    ctime = time.time()
    func2()
    print(time.time() - ctime)  # 20.41998553276062

(2)协程(Coroutine)

1.什么是协程

​ 协程是实现单线程下的并发,属于线程下的一种操作。

​ 协程的本质是,由用户自己控制一个任务遇到io阻塞就切换另一个任务去执行,保证该线程能够最大限度地处于就绪态,随时可以被CPU执行,相当于在用户程序级别将自己地I/O操作隐藏,以换取CPU地执行权限最大限度地属于本线程。

2.python下的线程及协程

​ python的线程属于内核级别地,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu地执行权限,切换其他线程运行)。

​ 单线程内开启协程,一旦遇到io,就会从应用程序级别(非操作系统)控制切换,以此来提升效率。

3.协程优缺点

优点:

​ 协程的切换开销更小,属于程序级别的切换(用户程序里自己保存多个控制流的上下文栈),操作系统感知不到,更轻量级;

​ 单线程内就可以实现并发的效果,最大限度利用CPU;

​ 修改共享的数据不需加锁。

缺点:

​ 协程的本质是单线程下,无法利用多核,必须在只有一个单线程里实现并发;

​ 协程指的是单个线程,且需要检测单线程下所有的I/O行为,而一旦协程出现阻塞,将会阻塞整个线程的运行。

(二)greenlet模块

​ 是一个初级的模块,需要手动切换,每一个切换点都要手动添加。

# 遇到io不会切,是一个初级模块,gevent模块基于它写的,处理io切换
from greenlet import greenlet
import time
def eat():
    print('我吃了一口')
    time.sleep(1)
    p.switch()
    print('我又吃了一口')
    p.switch()
def play():
    print('我玩了一会')
    e.switch()
    print('我又玩了一会')
if __name__ == '__main__':
    e = greenlet(eat)
    p = greenlet(play)
    e.switch()
'''
我吃了一口
我玩了一会
我又吃了一口
我又玩了一会
'''

(三)gevent模块+猴子补丁的使用

(1)Gevent是什么

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

(2)代码示例

# gevent基于greenlet写的,实现了遇见io自动切换
# 不使用猴子补丁
import gevent
import time
def eat(name):
    print('%s 吃了一口' % name)
    gevent.sleep(1)  # IO 操作
    print('%s 又吃了一口' % name)
def play(name):
    print('%s play了一会' % name)
    gevent.sleep(2)  # IO 操作
    print('%s 又play了一会' % name)
if __name__ == '__main__':
    ctime = time.time()
    e = gevent.spawn(eat, 'lxx')
    p = gevent.spawn(play, 'lxx')
    e.join()  # 等待e执行完成
    p.join()
    print('>>>mian<<<')
    print(time.time() - ctime)
    """
    lxx 吃了一口
    lxx play了一会
    lxx 又吃了一口
    lxx 又play了一会
    >>>mian<<<
    2.058576822280884
    """
    # 普通串行
    # ctime = time.time()
    # eat('lxx')
    # play('lxx')
    # print(time.time() - ctime)
    """
    lxx 吃了一口
    lxx 又吃了一口
    lxx play了一会
    lxx 又play了一会
    3.030064821243286
    """
# 使用猴子补丁,以后使用,这一句必须写
from gevent import monkey;monkey.patch_all()
import gevent
import time
def eat(name):
    print('%s 吃了一口' % name)
    time.sleep(1)  # IO 操作
    print('%s 又吃了一口' % name)
def play(name):
    print('%s play了一会' % name)
    time.sleep(2)  # IO 操作
    print('%s 又play了一会' % name)
if __name__ == '__main__':
    ctime = time.time()
    e = gevent.spawn(eat, 'lxx')
    p = gevent.spawn(play, 'lxx')
    e.join()
    p.join()
    print('>>>mian<<<')
    print(time.time() - ctime)
    """
    lxx 吃了一口
    lxx play了一会
    lxx 又吃了一口
    lxx 又play了一会
    >>>mian<<<
    2.01959228515625
    """

(四)asyncio模块了解

(1)python3.5以前写法:

import time
import asyncio
# 把普通函数变成协程函数
# python3.5以前写法:
@asyncio.coroutine
def task():
    print('开始了')
    yield from asyncio.sleep(1)  # asyncio.sleep(1)模拟io
    print('结束了')
loop = asyncio.get_event_loop()  # 获取一个时间循环对象
# 协程函数加括号,并不会真正的去执行,它需要提交给loop,让loop循环去执行
# 协程函数列表
ctime = time.time()
t = [task(), task()]
loop.run_until_complete(asyncio.wait(t))
loop.close()
print(time.time() - ctime)
"""
开始了
开始了
结束了
结束了
1.0000813007354736
"""

(2)python3.8的写法

import time
import asyncio
from threading import current_thread
# async表示协程函数的关键字,代替3.5之前的装饰器
async def task():
    print('开始了')
    print(current_thread().name)
    await asyncio.sleep(3)  # await等同于原来的yield from
    print('结束了')
async def task2():
    print('开始了')
    print(current_thread().name)
    await asyncio.sleep(2)
    print('结束了')
loop = asyncio.get_event_loop()

ctime = time.time()
t = [task(), task2()]
loop.run_until_complete(asyncio.wait(t))
loop.close()
print(time.time()-ctime)
"""
开始了
MainThread  # 在同一个写成内运行
开始了
MainThread
结束了
结束了
3.0020976066589355  # 并发运行,效率提高
"""

(五)必须会的I/O操作与模型

(1)IO操作的本质

​ 数据复制的过程中不会消耗CPU。

1.内存分为内核缓冲区和用户缓冲区;

2.用户的应用程序不能直接操作内核缓冲区,需要将数据从内核区拷贝到用户区才能使用;

3.而IO操作、网络请求加载到内存的数据一开始是放在内核缓冲区的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OmCSkApV-1598929794723)(D:\Onedrive\文档\01学习资料\全栈15博客笔记\python学习博客及作业\day040.并发编程之协程\IO操作.jpg)]

(2)IO模型

1.BIO—阻塞模式I/O

​ 用户进程从发起请求,到最终拿到数据前,一直挂起在等待;数据会由用户进程完成拷贝。

'''
举个例子:一个人去 商店买一把菜刀,
他到商店问老板有没有菜刀(发起系统调用)
如果有(表示在内核缓冲区有需要的数据)
老板直接把菜刀给买家(从内核缓冲区拷贝到用户缓冲区)
这个过程买家一直在等待

如果没有,商店老板会向工厂下订单(IO操作,等待数据准备好)
工厂把菜刀运给老板(进入到内核缓冲区)
老板把菜刀给买家(从内核缓冲区拷贝到用户缓冲区)
这个过程买家一直在等待
是同步io
'''

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p9hWvpfk-1598929794726)(D:\Onedrive\文档\01学习资料\全栈15博客笔记\python学习博客及作业\day040.并发编程之协程\002–BIO.jpg)]

2.NIO—非阻塞模式I/O

​ 用户进程发起请求,如果数据没有准备好,那么立刻告知用户进程未准备好;此时用户进程可以选择继续发起请求、或者先去做其他事情,稍后回来继续发送请求,直到被告知数据准备完毕,可以继续接收为止;数据会由用户进程完成拷贝。

'''
举个例子:一个人去 商店买一把菜刀,
他到商店问老板有没有菜刀(发起系统调用)
老板说没有,在向工厂进货(返回状态)
买家去别地方玩了会,又回来问,菜刀到了么(发起系统调用)
老板说还没有(返回状态)
买家又去玩了会(不断轮询)
最后一次再问,菜刀有了(数据准备好了)
老板把菜刀递给买家(从内核缓冲区拷贝到用户缓冲区)

整个过程轮询+等待:轮询时没有等待,可以做其他事,从内核缓冲区拷贝到用户缓冲区需要等待
是同步io

同一个线程,同一时刻只能监听一个socket,造成浪费,引入io多路复用,同时监听读个socket
'''

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZOznPhd0-1598929794730)(D:\Onedrive\文档\01学习资料\全栈15博客笔记\python学习博客及作业\day040.并发编程之协程\003–NIO.jpg)]

3.IO Multiplexing—I/O多路复用模型

​ 类似BIO,只不过找了一个代理,来挂起等待,并能同时监听多个请求;数据会由用户进程完成拷贝。

'''
举个例子:多个人去 一个商店买菜刀,
多个人给老板打电话,说我要买菜刀(发起系统调用)
老板把每个人都记录下来(放到select中)
老板去工厂进货(IO操作)
有货了,再挨个通知买到的人,来取刀(通知/返回可读条件)
买家来到商店等待,老板把到给买家(从内核缓冲区拷贝到用户缓冲区)

多路复用:老板可以同时接受很多请求(select模型最大1024个,epoll模型),
但是老板把到给买家这个过程,还需要等待,
是同步io


强调:
​ 1. 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
​ 2. 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
​ 结论: select的优势在于可以处理多个连接,不适用于单个连接
'''

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z3GiwmiN-1598929794734)(D:\Onedrive\文档\01学习资料\全栈15博客笔记\python学习博客及作业\day040.并发编程之协程\004–IO多路复用.jpg)]

4.AIO—异步I/O模型

​ 发起请求立刻得到回复,不用挂起等待;数据会由内核进程主动完成拷贝。

'''
举个例子:还是买菜刀
现在是网上下单到商店(系统调用)
商店确认(返回)
商店去进货(io操作)
商店收到货把货发个卖家(从内核缓冲区拷贝到用户缓冲区)
买家收到货(指定信号)

整个过程无等待
异步io

AIO框架在windows下使用windows IOCP技术,在Linux下使用epoll多路复用IO技术模拟异步IO

市面上多数的高并发框架,都没有使用异步io而是用的io多路复用,因为io多路复用技术很成熟且稳定,并且在实际的使用过程中,异步io并没有比io多路复用性能提升很多,没有达到很明显的程度
并且,真正的AIO编码难度比io多路复用高很多
'''

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KQd57nhY-1598929794736)(D:\Onedrive\文档\01学习资料\全栈15博客笔记\python学习博客及作业\day040.并发编程之协程\005–异步IO.jpg)]

5.select poll和epoll

①select, poll 和 epoll都是io多路复用技术

​ select, poll 和 epoll 都是io多路复用的机制。I/O多路复用就是通过一种机制,进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select, poll,espoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间.

②select

​ select函数监听的文件描述符分3类, 分别是writefds、readfds和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回,设为null即可),函数返回。当select函数返回后,可以通过遍历fdset 来找到就绪的描述符。

​ select目前几乎在所有的平台上支持,其良好的跨平台支持也是他的一个优点。select的一个缺点在于单搁进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

③poll

​ 不同于select使用三个位图表示fdset的方式,poll使用一个pollfd 的指针实现。

​ pollfd结构包含了要监视的event和发生的event,不再使用select “参数–值” 的传递方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

​ 从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在同一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

④epoll

​ epoll 是在Linux2.6内核中提出的,是之前的select和poll 的增强版本。相对于select和poll 来说,epoll更加灵活,没有描述符限制。espoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个时间表中,这样在用户空间和内核空间的copy只需一次。

⑤更好的例子理解

​ 老师检查同学作业,一个班50人,一个一个问,同学,作业写完了没?select,poll;

​ 老师检查同学作业,一个班50人,同学写完了,主动告诉老师,老师去检查作业,epoll。

⑥总结

​ 在高并发的情况下,连接活跃度不高,epoll 比select 好,如:网站http 的请求,连了就断掉。

​ 并发不高,同时连接很活跃,select比epoll好,如:websocket的连接,长连接,游戏开发。

三、补充:虚拟环境相关知识

1) 解决不同项目依赖的模块版本不同的问题
	pycharm中创建项目时选择
    -这个虚拟环境可不可以给其他项目使用(取决于你是否选择Make available to all projects);
    -基于系统解释器当前状态还是纯净状态来创建虚拟环境(Inherit global site-packages)2) 装模块:
	-cmd窗口下:pip3 install flask (装在哪个解释器环境下面需要确认好,是解释器的,还是pycharm的)
    -推荐使用pycharm:file-settings-Project:XXX-Python Interpreter
    -pycharm下的terminal下安装(相当于cmd),比cmd好在,它有个路径提示
    
3) 现在用了虚拟环境如何切换到系统环境
	pycharm:file-settings-Project:XXX-Python Interpreter

4) 环境变量的作用
	-把一个路径加入到环境变量,以后该路径下的命令,可以在任意位置执行。

你可能感兴趣的:(python基础学习,python)