在正式进入学习前需要弄清楚三组概念:
- 同步和异步
- 串行和并行
- 并行和并发
- 同步:做一件事情一定要看着它、把它做完,然后再开始下一件事。
- 异步:做一件事情可以把它交给别人做(不会一直等着它),继续做下一件事。
- 串行:一件事一件事的做。
- 并行:两件事同时做,即同一个时间点有多个人在干活。
- 并行:同一时间点有多个任务在执行。
- 并发:一段时间内,有多个任务轮流执行;且同一个时间点依然只执行一个任务。
通常大家说的高并发指的就是【并发】这个概念,充分压榨 CPU
资源。
Python
中实现并发的手段有三个:进程、线程、协程。
-
CPU
在执行任务时,整整干活的是线程,当一个线程遇到IO
时会交出执行权,切换到另个一个线程。 -
协程
Coroutine
,也称为微线程,是一种用户态内的上下文切换技术,即在一个线程内切换被执行的代码块。代码级别的切换。
示例:同步执行的代码,代码块依次执行
def f1():
print(1)
print(2)
def f2():
print(3)
print(4)
f1()
f2()
# 依次打印:1 2 3 4
示例:使用 yield
实现的协程,实现代码块的切换执行。
def f1():
yield 1
yield from f2()
yield 2
def f2():
yield 3
yield 4
generator = f1() # f1()是生成器
for i in generator:
print(i) # 依次打印:1 3 4 2
第三方包实现协程
示例:使用第三方包 greenlet
实现协程
from greenlet import greenlet
def f1():
print(1)
gr2.switch()
print(2)
gr2.switch()
def f2():
print(3)
gr1.switch()
print(4)
gr1 = greenlet(f1)
gr2 = greenlet(f2)
gr1.switch() # 依次打印:1 3 2 4
示例:使用第三方包 gevent
实现协程(内部基于 greenlet
),遇到 IO
会自动切换。
import gevent
def f1():
print(1)
gevent.sleep(2) # 注意不使用time.sleep(2)
print(2)
def f2():
print(3)
gevent.sleep(2)
print(4)
g1 = gevent.spawn(f1)
g2 = gevent.spawn(f2)
gevent.joinall([g1, g2]) # 等待所有函数运行结束,依次打印1 3 -睡2s- 2 4
asyncio 实现协程
gevent
基于greenlet
猴子补丁:from gevent import monkey; monkey.patch_all()
Python3.4
之前官方未提供协程的类库,一般使用 gevent
实现协程达到异步编程的目的。
Python3.4
之后官方推出 asyncio
模块,有如下几个特点:
- 单线程
- 事件循环
- 适用于
IO
密集型场景
示例:通过 asyncio
实现协程
import asyncio
@asyncio.coroutine
def f1():
print(1)
yield from asyncio.sleep(2) # 遇到IO睡2s
print(2)
@asyncio.coroutine
def f2():
print(3)
yield from asyncio.sleep(2) # 遇到IO睡2s
print(4)
tasks = [
asyncio.ensure_future(f1()),
asyncio.ensure_future(f2())
]
loop = asyncio.get_event_loop() # 得到一个事件循环
loop.run_until_complete(asyncio.wait(tasks)) # 等待所有任务执行完毕
# 打印结果:1 3 -睡2s- 2 4
注意
asyncio
内部是基于yield
实现的协程,本质上在事件循环内切换执行任务。@asyncio.coroutine
这个装饰器在Python3.8
中将会被弃用,推荐使用async def
。
Python3.5
官方推出async
和await
关键字,目的是让协程代码可以更加简洁。
import asyncio
async def f1():
print(1)
await asyncio.sleep(2) # 等 可以等待的对象
print(2)
async def f2():
print(3)
await asyncio.sleep(2)
print(4)
tasks = [
asyncio.ensure_future(f1()),
asyncio.ensure_future(f2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
# 打印结果:1 3 -睡2s- 2 4
注意
await
只能用在async def
内,但在ipython
中可以直接使用
- 协程的主要用途在
IO
密集型场景,本节通过爬虫带着大家体验下异步爬虫的高效。
示例:同步爬虫
import requests
def download_img(url):
file_name = url.rsplit('/')[-1]
print(f"下载图片:{file_name}")
response = requests.get(url)
with open(file_name, mode='wb') as file:
file.write(response.content)
print(f"下载完成:{file_name}")
def main():
urls = [
"https://tenfei05.cfp.cn/creative/vcg/800/new/VCG41560336195.jpg",
"https://tenfei03.cfp.cn/creative/vcg/800/new/VCG41688057449.jpg",
]
for item in urls:
download_img(item)
main()
# 遍历下载列表,第一张图片下载结束,再开始下载下一张图片。
示例:使用 asyncio
异步下载图片
import asyncio
import aiohttp
async def download_img(session, url):
file_name = url.rsplit('/')[-1]
print(f"下载图片:{file_name}")
response = await session.get(url, ssl=False)
content = await response.content.read()
with open(file_name, mode='wb') as file:
file.write(content)
print(f"下载完成:{file_name}")
async def main():
urls = [
"https://tenfei05.cfp.cn/creative/vcg/800/new/VCG41560336195.jpg",
"https://tenfei03.cfp.cn/creative/vcg/800/new/VCG41688057449.jpg",
]
async with aiohttp.ClientSession() as session:
tasks = [asyncio.ensure_future(download_img(session, url)) for url in urls]
await asyncio.wait(tasks)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
总结
IO
密集型场景,使用协程优势明显。IO
等待时切换执行其他任务,进而提高效率。- 基于
async
&await
关键字的协程可以实现异步编程,这也是目前的主流方式。
- 事件循环是
asyncio
的核心。在Python
异步应用中,每一个异步任务都是在事件循环中被执行的。 - 简单理解,可以把事件循环当成一个
while
循环,循环内部执行任务,当一个任务遇到IO
挂起时,就会立即切换执行另一个任务,如此反复,在特定条件下会终止循环。
官网:https://docs.python.org/3/library/asyncio-eventloop.html
- 图解事件循环:
注意:程序遇到await
关键词时,程序会挂起,事件循环会再找一个任务开始执行。
- 创建事件循环
import asyncio
asyncio.get_event_loop()
- 推荐使用
asyncio.run()
来运行事件循环
import asyncio
async def f1():
await asyncio.sleep(5)
# loop = asyncio.get_event_loop()
# loop.run_until_complete(f1())
# run等价于上面两行代码
asyncio.run(f1())
协程函数和协程对象
- 协程函数:通过
async def
定义的函数 - 协程对象:协程函数执行后返回的对象
注意:
- 调用协程函数,函数内部代码不会执行,只是会返回一个协程对象。
- 想要执行协程函数内部代码,需要配合事件循环。
import asyncio
async def f1():
await asyncio.sleep(5)
# loop = asyncio.get_event_loop() # 获取一个事件循环
# loop.run_until_complete(f1()) # 将协程函数当做任务添加到事件循环中
# run等价于上面两行代码
asyncio.run(f1())
注意:
run
会新建一个事件循环,任务结束时会自动关闭它。run
应该作为异步程序的唯一入口,只能被调用一次。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)