asyncio
模块提供了使用协程构建并发应用的工具。它使用一种单线程单进程的方式实现并发,应用的各个部分彼此合作,可以显示的切换任务,一般会在程序阻塞I/O操作的时候发生上下文切换如等待读写文件,或者请求网络。同时asyncio
也支持调度代码在将来的某个特定事件运行,从而支持一个协程等待另一个协程完成,以处理系统信号和识别其他一些事件。
对于其他的并发模型大多数采用的都是线性的方式编写。并且依赖于语言运行时系统或操作系统底层的底层线程或进程来适当的改变上下文,而基于asyncio
的应用要求应用代码显示的处理上下文切换。asyncio
提供的框架以事件循环为中心,程序开启一个无限的循环,程序会把一些函数注册到事件循环当中,当满足事件发生的时候,调用相应的协程函数。
锁可以用来保护一个共享资源的访问,只有锁的持有者可以使用这个资源。如果有多个请求需要用到这个锁,那么会将其阻塞,以保证一次只有一个持有者。它是在解锁状态下创建的,它有两个基本方法:acquire()
和release()
。当状态解除锁定时,acquire()
将状态更改为locked
并立即返回。当状态被锁定时,acquire()
阻塞,直到另外一个协同程序中的release()
调用将其改为解锁,然后acquire()
调用将其重置为锁定并返回。
release()
方法只能在锁定状态下调用,它将状态更改为解锁并立即返回。如果试图释放一个未锁定的锁,将引发一个RuntimeError
。此类不是线程安全的!请看下面例子:
import asyncio
from functools import partial
async def release(lock):
print("relase-1: {}".format(lock.locked()))
lock.release()
print("relase-2: {}".format(lock.locked()))
async def coroutine_1(lock):
print("进入coroutine_1: {}".format(lock.locked()))
await lock.acquire()
print("coroutine_1此时锁的状态: {}".format(lock.locked()))
lock.release()
print("coroutine_1此时锁的状态-2: {}".format(lock.locked()))
async def coroutine_2(lock):
print("进入coroutine_2: {}".format(lock.locked()))
async with lock:
print("coroutine_2此时锁的状态: {}".format(lock.locked()))
print("coroutine_2此时锁的状态-2: {}".format(lock.locked()))
async def coroutine_3(lock):
print("进入coroutine_3: {}".format(lock.locked()))
try:
lock.release()
except RuntimeError as e:
print("触发RuntimeError错误")
async def main(loop):
print("进入主协程")
# 创建一个锁
lock = asyncio.Lock()
loop.call_later(0.1, partial(release, lock))
await asyncio.wait([coroutine_1(lock), coroutine_2(lock), coroutine_3(lock)])
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main(loop))
finally:
loop.close()
可能输出的结果如下:
进入主协程
进入coroutine_1: False
coroutine_1此时锁的状态: True
coroutine_1此时锁的状态-2: False
进入coroutine_2: False
coroutine_2此时锁的状态: True
coroutine_2此时锁的状态-2: False
进入coroutine_3: False
触发RuntimeError错误
通过上面代码可以得出以下结论:
lock.acquire()
需要加await
lock.release()
不需要加await
coroutine_1
、coroutine_2
、coroutine_3
是并发执行的cortouine_1
中使用await lock.acquire()
加锁,这种方式需要在结束的使用调用lock.release()
来释放锁;二是像coroutine_2
中使用async with lock
异步上下文进行锁定,这种方式不需要手动进行释放操作acquire
进行加锁,却试图使用release
去释放锁,那么将会引发RuntimeError
异常常用方法如下:
locked()
: 如果获得了锁,则返回True,否则返回Falseacquire()
: 获取锁。此方法将一直锁定直到解锁,将其设置为locked
,并返回Truerelease()
: 释放锁。当锁被锁定,重置为解锁。无返回值asyncio.Event
基于threading.Event
,允许多个消费者等待某个事件发生,而不必寻找一个特定值与通知关联!事件管理一个标识,该标识可以通过set()
方法设置为True,通过clear()
方法重置为False,wait()
方法标阻塞,直到标记为True。该标识最初为False,此类不是线程安全的!
import asyncio
from functools import partial
def callback(event):
print("callback当前Event状态:{}".format(event.is_set()))
event.set()
print("callback当前Event状态-2:{}".format(event.is_set()))
async def child(name, event):
print("{}当前Event状态: {}".format(name, event.is_set()))
await event.wait()
print("{}当前Event状态: {}".format(name, event.is_set()))
async def main(loop):
event = asyncio.Event()
print("Event创建之后的状态为: {}".format(event.is_set()))
loop.call_later(0.1, partial(callback, event))
tasks = [child("name{}".format(i), event) for i in range(5)]
await asyncio.wait(tasks)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main(loop))
finally:
loop.close()
可能输出的结果如下:
Event创建之后的状态为: False
name2当前Event状态: False
name3当前Event状态: False
name4当前Event状态: False
name0当前Event状态: False
name1当前Event状态: False
callback当前Event状态:False
callback当前Event状态-2:True
name2当前Event状态: True
name3当前Event状态: True
name4当前Event状态: True
name0当前Event状态: True
name1当前Event状态: True
从结果可以看出,一个触发了事件,child
就会立即启动,不需要得到事件对象上唯一的锁!
常用方法如下:
clear()
: 将内部标识重置为Falseis_set()
: 返回内部标识状态set()
: 将内部标识设置为True,所有等待它为True的协程将被唤醒wait()
: 阻塞,直到内部标识设置为TrueCondition
的做法与Event
类似,只不过不是通知所有的协程等待的协程,被唤醒的等待协程数目由notify()
的一个参数控制,此类不是线程安全的。请看如下代码:
import asyncio
async def child(cond, name):
print("{}进入child, cond状态为: {}".format(name, cond.locked()))
async with cond:
await cond.wait()
print("{}在child开始执行, cond状态为: {}".format(name, cond.locked()))
print("{}在child执行完毕, cond状态为: {}".format(name, cond.locked()))
async def coroutine_1(cond):
print("进入coroutine_1, cond状态为: {}".format(cond.locked()))
await asyncio.sleep(2)
for i in range(1, 4):
async with cond:
print("coroutine_1-资源变得可用")
cond.notify(n=i)
await asyncio.sleep(1)
async def coroutine_2(cond):
print("进入coroutine_2, cond状态为: {}".format(cond.locked()))
await asyncio.sleep(2)
async with cond:
print("coroutine_2-资源变得可用")
cond.notify_all()
async def main(loop):
print("进入main协程")
cond = asyncio.Condition()
print("condition的状态为:{}".format(cond.locked()))
loop.create_task(coroutine_1(cond))
tasks = [child(cond, "name{}".format(i)) for i in range(1, 6)]
await asyncio.wait(tasks)
loop.create_task(coroutine_2(cond))
tasks = [child(cond, "name{}".format(i)) for i in range(6, 11)]
await asyncio.wait(tasks)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main(loop))
finally:
loop.close()
输出结果如下:
进入main协程
condition的状态为:False
进入coroutine_1, cond状态为: False
name3进入child, cond状态为: False
name4进入child, cond状态为: False
name1进入child, cond状态为: False
name5进入child, cond状态为: False
name2进入child, cond状态为: False
coroutine_1-资源变得可用
name3在child开始执行, cond状态为: True
name3在child执行完毕, cond状态为: False
coroutine_1-资源变得可用
name4在child开始执行, cond状态为: True
name4在child执行完毕, cond状态为: False
name1在child开始执行, cond状态为: True
name1在child执行完毕, cond状态为: False
coroutine_1-资源变得可用
name5在child开始执行, cond状态为: True
name5在child执行完毕, cond状态为: False
name2在child开始执行, cond状态为: True
name2在child执行完毕, cond状态为: False
进入coroutine_2, cond状态为: False
name10进入child, cond状态为: False
name9进入child, cond状态为: False
name8进入child, cond状态为: False
name6进入child, cond状态为: False
name7进入child, cond状态为: False
coroutine_2-资源变得可用
name10在child开始执行, cond状态为: True
name10在child执行完毕, cond状态为: False
name9在child开始执行, cond状态为: True
name9在child执行完毕, cond状态为: False
name8在child开始执行, cond状态为: True
name8在child执行完毕, cond状态为: False
name6在child开始执行, cond状态为: True
name6在child执行完毕, cond状态为: False
name7在child开始执行, cond状态为: True
name7在child执行完毕, cond状态为: False
通过输出结果进行分析,使用notify
方法每次通知n
个child,notify_all
方法一次性的通知全部child。
常用方法如下:
acquire()
: 获取锁。此方法将一直锁定直到解锁,将其设置为locked
,并返回Truenotify(n=1)
: 默认情况下,唤醒一个在此条件下等待的协程(如果有的话)。如果调用的协程在调用此方法时没有获得锁,则会引发RuntimeError
异常。此方法最多唤醒n个等待条件变量的协程locked()
: 如果获得了锁,则返回True,否则返回Falsenotify_all()
: 唤醒所有此条件下等待的协程。如果调用的协程在调用此方法时没有获得锁,则会引发RuntimeError
异常。release()
: 释放锁。当锁被锁定,重置为解锁。无返回值wait()
: 阻塞,直到内部标识设置为Truewait_for(predicate)
: 阻塞,直到predicate
的返回值为Truewait_for
使用如下:
async def child(cond, name):
print("{}进入child, cond状态为: {}".format(name, cond.locked()))
async with cond:
await cond.wait_for(coroutine)
print("{}在child开始执行, cond状态为: {}".format(name, cond.locked()))
print("{}在child执行完毕, cond状态为: {}".format(name, cond.locked()))
def coroutine():
time.sleep(2)
return True
Condition
对锁的一些操作与Lock
一样!
Queue
Queue(maxsize=0, *, loop=None)
先进先出队列,如果maxsize
小于或等于0,则队列大小为无穷大。如果它是一个大于0的整数,那么当队列达到maxsize
时,put()
的yield将阻塞,直到get()
删除一个项目为止。此类是线程不安全的!
与标准库Queue
不同,qsize()
可以可靠的直到此队列的大小,因为在调用qsize()
和队列执行操作之间不会中断单线程异步应用程序。请看如下列子:
import asyncio
async def get_item(queue):
item = await queue.get()
print("get_item: {}".format(item))
return item
async def main():
queue = asyncio.Queue()
[await queue.put("name{}".format(i)) for i in range(5)]
tasks = [get_item(queue) for _ in range(5)]
await asyncio.wait(tasks)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()
输出结果如下:
get_item: name0
get_item: name1
get_item: name2
get_item: name3
get_item: name4
LifoQueue
LifoQueue(maxsize=0, *, loop=None)
为Queue
的子类,先进后出队列。将上面代码修改为使用LifoQueue
,如下:
import asyncio
async def get_item(queue):
item = await queue.get()
print("get_item: {}".format(item))
return item
async def main():
queue = asyncio.LifoQueue()
[await queue.put("name{}".format(i)) for i in range(5)]
tasks = [get_item(queue) for _ in range(5)]
await asyncio.wait(tasks)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()
输出结果如下:
get_item: name4
get_item: name3
get_item: name2
get_item: name1
get_item: name0
PriorityQueue
PriorityQueue(maxsize=0, *, loop=None)
为Queue
的子类,优先级队列,优先级的值越小,越先执行。put()
操作与上面的两种队列有所不同,"项目"的形式是一个元组:(priority_number, data),priority_number
为优先级数值!将上面代码修改为使用PriorityQueue
,如下:
import asyncio
import random
async def get_item(queue):
item = await queue.get()
print("get_item: {}".format(item))
return item
async def main():
queue = asyncio.PriorityQueue()
[await queue.put((random.randint(1, 6), "name{}".format(i))) for i in range(5)]
tasks = [get_item(queue) for _ in range(5)]
await asyncio.wait(tasks)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()
输出结果如下:
get_item: (3, 'name4')
get_item: (4, 'name1')
get_item: (6, 'name0')
get_item: (6, 'name2')
get_item: (6, 'name3')
Queue
常用方法如下:
empty()
: 如果队列为空返回True,否则返回Falsefull()
: 如果队列中有maxsize
个项目,则返回True,否则返回Falseget()
: 从队列中删除并返回一个项目,如果队列为空,则会阻塞直到有一个项目可用。需使用await
阻塞get_nowait()
: 从队列中删除并返回一个项目,如果队列为空,则会引发QueueEmpty
异常join()
: 阻塞直到队列中的所有项目都已获得并处理,需使用await
阻塞put(item)
: 将项目放入队列,如果队列已满,则会阻塞直到有可用插槽为止。需使用await
阻塞put_nowait(item)
: 将项目放入队列而不会阻塞qsize()
: 返回队列中的项目数task_done()
: 表示先前排队的任务已完成maxsize
: 队列中允许的项目数Semaphore(value=1, *, loop=None)
通过并发量可以控制协程的并发数,爬虫操作中使用该方法减少并发量,可以减少对服务器的压力。请看如下代码:
import time
import asyncio
async def run(sem, name):
async with sem:
print("{}进入run协程, {}".format(name, time.time()))
await asyncio.sleep(1)
async def main():
sem = asyncio.Semaphore(5)
tasks = [run(sem, "name{}".format(i)) for i in range(18)]
await asyncio.wait(tasks)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()
输出结果如下:
name3进入run协程, 1585126139.9968233
name9进入run协程, 1585126139.9968233
name15进入run协程, 1585126139.9968233
name4进入run协程, 1585126139.9968233
name10进入run协程, 1585126139.9968233
name16进入run协程, 1585126140.9972842
name1进入run协程, 1585126140.9972842
name5进入run协程, 1585126140.9972842
name11进入run协程, 1585126140.9972842
name17进入run协程, 1585126140.9972842
name0进入run协程, 1585126141.9978023
name6进入run协程, 1585126141.9978023
name12进入run协程, 1585126141.9978023
name7进入run协程, 1585126141.9978023
name13进入run协程, 1585126141.9978023
name2进入run协程, 1585126142.9983144
name8进入run协程, 1585126142.9983144
name14进入run协程, 1585126142.9983144
从结果可以看到,并发量为5!
常用方法如下:
locked()
: 如果获得了锁,则返回True,否则返回Falseacquire()
: 获取锁。此方法将一直锁定直到解锁,将其设置为locked
,并返回Truerelease()
: 释放锁。当锁被锁定,重置为解锁。无返回值多线程与队列相关知识:https://blog.csdn.net/y472360651/article/details/73495122
自此,Over~