协程:当程序执行的某一个任务遇到了IO操作时(处于阻塞状态),不让CPU切换走(就是不让CPU去执行其他程序),而是选择性的切换到其他任务上,让CPU执行新的任务,当原来的任务不处于阻塞状态后,CPU可以快速的回到之前的任务继续执行,这样就不用让原本的程序去排队等待CPU调度。
微观上看,任务是一个一个的切换执行,切换条件就是某一个任务有IO操作, 而宏观上,我们看到的是多个任务一起执行,这就是多任务异步操作。上面的一切的前提就是单线程的情况下,因为多线程可以多个线程同时干多件事。
import time
def func():
print('first, hi!')
# 让程序睡眠3秒钟,此时线程处于阻塞状态,CPU不为线程工作
# 当我们爬取一个网页时,向一个url发送请求,会通过网络传输将请求发送到服务器
# 然后服务器会处理请求、准备数据、将数据通过网络传输回客户端等工作
# 这一系列的操作也会耗费时间,所以在从发送请求开始,到接收服务器返回的数据这一段时间内
# 即在网络请求返回数据之前,程序也处于阻塞状态
# 程序进行处于IO操作时是处于阻塞状态的
time.sleep(3)
print('second, hello..')
if __name__ == '__main__':
func()
有四种方式,但这里只选择其中的一种,如以下代码所示:
import asyncio
# 这种写法就是普通的函数
# def func():
# print('你好,我是张三!')
#
#
# if __name__ == '__main__':
# func()
# 在函数前面加async关键字,就表明该函数是异步协程函数
async def func():
print('你好,我是张三!')
if __name__ == '__main__':
# func() # 如果直接调用,会得到一个警告:RuntimeWarning: ...
g = func() # 此时函数是一个异步协程函数,执行函数得到一个协程对象
"""
输出:
<coroutine object func at 0x000001F823066960>
sys:1: RuntimeWarning: coroutine 'func' was never awaited
"""
print(g)
asyncio.run(g) # 协程程序的运行需要asyncio模块的支持
import asyncio
import time
# 在函数前面加async关键字,就表明该函数是异步协程函数
async def func1():
print('你好,我是张三!')
time.sleep(3)
print('你好,我是张三!')
async def func2():
print('你好,我是李四!')
time.sleep(2)
print('你好,我是李四!')
async def func3():
print('你好,我是王五!')
time.sleep(4)
print('你好,我是王五!')
if __name__ == '__main__':
f1 = func1()
f2 = func2()
f3 = func3()
# 把多个异步任务放到一个列表中
tasks = [f1, f2, f3]
t1 = time.time()
# 一次性启动多个异步任务(协程)
asyncio.run(asyncio.wait(tasks))
t2 = time.time()
print(t2 - t1)
上面三个函数是异步协程操作,理论上执行时间应该会小于9秒,因为异步任务会在某一个任务阻塞时去调用其他任务,但是观察上述代码执行时间,发现和同步执行三个函数效果一样,都是用了9秒多,如下图。出现这种的情况的原因是:函数里的time.sleep()是同步操作,而异步协程函数中出现同步操作的时候,异步就中断了,也就是说,当异步函数中有同步操作时,CPU不会切换去调用其他任务,而是像同步函数那样,执行完一个任务再去执行另一个任务(在这个例子中,就是执行完func1,再执行func2,再执行func3)。
修改上述代码,实现异步操作效果,如下:
import asyncio
import time
# 在函数前面加async关键字,就表明该函数是异步协程函数
async def func1():
print('你好,我是张三!')
# time.sleep(3) # 异步程序中出现同步操作,会中断异步,即不会切换任务执行
# 异步操作代码,表示挂起任务,让任务睡眠3秒,然后切换CPU去执行其他任务
await asyncio.sleep(3)
print('你好,我是张三!')
async def func2():
print('你好,我是李四!')
# time.sleep(2)
await asyncio.sleep(2)
print('你好,我是李四!')
async def func3():
print('你好,我是王五!')
# time.sleep(4)
await asyncio.sleep(4)
print('你好,我是王五!')
# 一般不会直接像下面那样调用多个异步任务,而是把它包装在一个异步协程函数里
# if __name__ == '__main__':
# f1 = func1()
# f2 = func2()
# f3 = func3()
# # 把多个异步任务放到一个列表中
# tasks = [f1, f2, f3]
# t1 = time.time()
# # 一次性启动多个异步任务(协程)
# asyncio.run(asyncio.wait(tasks))
# t2 = time.time()
# print(t2 - t1)
async def main():
# 写法一(不推荐)
# await 都是写在异步协程函数里,即与async配套使用
# await后一般跟协程对象、task等对象
# await表示挂起某个异步任务,即是执行某个异步任务
# await asyncio.create_task(func1())
# await asyncio.create_task(func2())
# await asyncio.create_task(func3())
# 写法二(推荐)
tasks = [
# asyncio.create_task(func1()) 把协程对象包装成task对象
asyncio.create_task(func1()),
asyncio.create_task(func2()),
asyncio.create_task(func3())
]
# 这里await作用和上面一样,表示挂起协程对象,即会异步执行tasks列表中的异步任务
await asyncio.wait(tasks)
if __name__ == '__main__':
t1 = time.time()
asyncio.run(main())
t2 = time.time()
print(t2 - t1)
import asyncio
async def download(url):
print('开始下载...')
await asyncio.sleep(2)
print('下载完成!')
async def main():
tasks = []
urls = ['url1', 'url2', 'url3']
for url in urls:
d = download(url) # 得到一个异步协程对象
# asyncio.create_task(d) 把协程对象包装成task对象
tasks.append(asyncio.create_task(d))
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())
以下代码是根据多个图片地址异步下载图片
import asyncio
# 下载命令:pip install aiohttp
import aiohttp
# 图片地址
urls = [
"https://img95.699pic.com/photo/50165/7667.jpg_wh860.jpg",
"https://bpic.588ku.com/back_origin_min_pic/20/04/19/f753e29e3dbe2ad75b8f6d6053199faa.jpg"
]
async def download(url):
file_name = url.rsplit('/', 1)[1]
# aiohttp.ClientSession()对象等价于requests模块,所以也有get、post方法
# 且用法差不多
async with aiohttp.ClientSession() as req: # => req = aiohttp.ClientSession()
# 因为是异步操作,所以要加上async关键字
# with的作用和文件操作中的with类似,可以管理上下文,在使用完req对象之后会自动关闭
# req.get(url) 发送请求获取图片数据
async with req.get(url) as resp: # => resp = req.get(url)
# 这里的文件读写操作也是IO操作,也是会造成阻塞,所以也可以通过异步协程来完成
# 具体可以学习aiofiles模块来实现
with open(file_name, mode='wb') as f:
# resp.content.read()是异步操作,所以前面要加await表示挂起
# 挂起的意思就是resp.content.read()什么时候有东西了什么时候写入文件
# 即什么时候有需要的内容了什么时候进行对应的操作
# resp.content.read() 表示以字节的形式读取返回的数据的内容
# 在这里就是读取图片的字节数据,然后存入文件,即保存图片数据
f.write(await resp.content.read())
# req.close() 使用with之后不用手动写上这句话
print(file_name, '下载完成')
async def main():
tasks = [asyncio.create_task(download(url)) for url in urls]
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())