我有一个list
的awaitables
,我想传递给asyncio.AbstractEventLoop
,但我需要节流请求第三方API。
我想避免等待传递future
给循环的东西,因为与此同时,我阻止了循环等待。我有什么选择?Semaphores
并ThreadPools
限制同时运行的数量,但这不是我的问题。我需要将请求限制为100 /
sec,但是完成请求所需的时间并不重要。
这是使用标准库的非常简洁(不起作用)的示例,演示了该问题。应该以100 /秒的速度节流,但以116.651 /秒的速度节流。
在asyncio中限制异步请求调度的最佳方法是 什么?
工作代码:
import asyncio
from threading import Lock
class PTBNL:
def __init__(self):
self._req_id_seq = 0
self._futures = {}
self._results = {}
self.token_bucket = TokenBucket()
self.token_bucket.set_rate(100)
def run(self, *awaitables):
loop = asyncio.get_event_loop()
if not awaitables:
loop.run_forever()
elif len(awaitables) == 1:
return loop.run_until_complete(*awaitables)
else:
future = asyncio.gather(*awaitables)
return loop.run_until_complete(future)
def sleep(self, secs) -> True:
self.run(asyncio.sleep(secs))
return True
def get_req_id(self) -> int:
new_id = self._req_id_seq
self._req_id_seq += 1
return new_id
def start_req(self, key):
loop = asyncio.get_event_loop()
future = loop.create_future()
self._futures[key] = future
return future
def end_req(self, key, result=None):
future = self._futures.pop(key, None)
if future:
if result is None:
result = self._results.pop(key, [])
if not future.done():
future.set_result(result)
def req_data(self, req_id, obj):
# Do Some Work Here
self.req_data_end(req_id)
pass
def req_data_end(self, req_id):
print(req_id, " has ended")
self.end_req(req_id)
async def req_data_async(self, obj):
req_id = self.get_req_id()
future = self.start_req(req_id)
self.req_data(req_id, obj)
await future
return future.result()
async def req_data_batch_async(self, contracts):
futures = []
FLAG = False
for contract in contracts:
req_id = self.get_req_id()
future = self.start_req(req_id)
futures.append(future)
nap = self.token_bucket.consume(1)
if FLAG is False:
FLAG = True
start = asyncio.get_event_loop().time()
asyncio.get_event_loop().call_later(nap, self.req_data, req_id, contract)
await asyncio.gather(*futures)
elapsed = asyncio.get_event_loop().time() - start
return futures, len(contracts)/elapsed
class TokenBucket:
def __init__(self):
self.tokens = 0
self.rate = 0
self.last = asyncio.get_event_loop().time()
self.lock = Lock()
def set_rate(self, rate):
with self.lock:
self.rate = rate
self.tokens = self.rate
def consume(self, tokens):
with self.lock:
if not self.rate:
return 0
now = asyncio.get_event_loop().time()
lapse = now - self.last
self.last = now
self.tokens += lapse * self.rate
if self.tokens > self.rate:
self.tokens = self.rate
self.tokens -= tokens
if self.tokens >= 0:
return 0
else:
return -self.tokens / self.rate
if __name__ == '__main__':
asyncio.get_event_loop().set_debug(True)
app = PTBNL()
objs = [obj for obj in range(500)]
l,t = app.run(app.req_data_batch_async(objs))
print(l)
print(t)
编辑:我已经添加了一个TrottleTestApp
使用信号量的简单示例,但是仍然无法限制执行:
import asyncio
import time
class ThrottleTestApp:
def __init__(self):
self._req_id_seq = 0
self._futures = {}
self._results = {}
self.sem = asyncio.Semaphore()
async def allow_requests(self, sem):
"""Permit 100 requests per second; call
loop.create_task(allow_requests())
at the beginning of the program to start this routine. That call returns
a task handle that can be canceled to end this routine.
asyncio.Semaphore doesn't give us a great way to get at the value other
than accessing sem._value. We do that here, but creating a wrapper that
adds a current_value method would make this cleaner"""
while True:
while sem._value < 100: sem.release()
await asyncio.sleep(1) # Or spread more evenly
# with a shorter sleep and
# increasing the value less
async def do_request(self, req_id, obj):
await self.sem.acquire()
# this is the work for the request
self.req_data(req_id, obj)
def run(self, *awaitables):
loop = asyncio.get_event_loop()
if not awaitables:
loop.run_forever()
elif len(awaitables) == 1:
return loop.run_until_complete(*awaitables)
else:
future = asyncio.gather(*awaitables)
return loop.run_until_complete(future)
def sleep(self, secs: [float]=0.02) -> True:
self.run(asyncio.sleep(secs))
return True
def get_req_id(self) -> int:
new_id = self._req_id_seq
self._req_id_seq += 1
return new_id
def start_req(self, key):
loop = asyncio.get_event_loop()
future = loop.create_future()
self._futures[key] = future
return future
def end_req(self, key, result=None):
future = self._futures.pop(key, None)
if future:
if result is None:
result = self._results.pop(key, [])
if not future.done():
future.set_result(result)
def req_data(self, req_id, obj):
# This is the method that "does" something
self.req_data_end(req_id)
pass
def req_data_end(self, req_id):
print(req_id, " has ended")
self.end_req(req_id)
async def req_data_batch_async(self, objs):
futures = []
FLAG = False
for obj in objs:
req_id = self.get_req_id()
future = self.start_req(req_id)
futures.append(future)
if FLAG is False:
FLAG = True
start = time.time()
self.do_request(req_id, obj)
await asyncio.gather(*futures)
elapsed = time.time() - start
print("Roughly %s per second" % (len(objs)/elapsed))
return futures
if __name__ == '__main__':
asyncio.get_event_loop().set_debug(True)
app = ThrottleTestApp()
objs = [obj for obj in range(10000)]
app.run(app.req_data_batch_async(objs))
您可以通过实现漏斗算法来做到这一点:
import asyncio
import contextlib
import collections
import time
from types import TracebackType
from typing import Dict, Optional, Type
try: # Python 3.7
base = contextlib.AbstractAsyncContextManager
_current_task = asyncio.current_task
except AttributeError:
base = object # type: ignore
_current_task = asyncio.Task.current_task # type: ignore
class AsyncLeakyBucket(base):
"""A leaky bucket rate limiter.
Allows up to max_rate / time_period acquisitions before blocking.
time_period is measured in seconds; the default is 60.
"""
def __init__(
self,
max_rate: float,
time_period: float = 60,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> None:
self._loop = loop
self._max_level = max_rate
self._rate_per_sec = max_rate / time_period
self._level = 0.0
self._last_check = 0.0
# queue of waiting futures to signal capacity to
self._waiters: Dict[asyncio.Task, asyncio.Future] = collections.OrderedDict()
def _leak(self) -> None:
"""Drip out capacity from the bucket."""
if self._level:
# drip out enough level for the elapsed time since
# we last checked
elapsed = time.time() - self._last_check
decrement = elapsed * self._rate_per_sec
self._level = max(self._level - decrement, 0)
self._last_check = time.time()
def has_capacity(self, amount: float = 1) -> bool:
"""Check if there is enough space remaining in the bucket"""
self._leak()
requested = self._level + amount
# if there are tasks waiting for capacity, signal to the first
# there there may be some now (they won't wake up until this task
# yields with an await)
if requested < self._max_level:
for fut in self._waiters.values():
if not fut.done():
fut.set_result(True)
break
return self._level + amount <= self._max_level
async def acquire(self, amount: float = 1) -> None:
"""Acquire space in the bucket.
If the bucket is full, block until there is space.
"""
if amount > self._max_level:
raise ValueError("Can't acquire more than the bucket capacity")
loop = self._loop or asyncio.get_event_loop()
task = _current_task(loop)
assert task is not None
while not self.has_capacity(amount):
# wait for the next drip to have left the bucket
# add a future to the _waiters map to be notified
# 'early' if capacity has come up
fut = loop.create_future()
self._waiters[task] = fut
try:
await asyncio.wait_for(
asyncio.shield(fut),
1 / self._rate_per_sec * amount,
loop=loop
)
except asyncio.TimeoutError:
pass
fut.cancel()
self._waiters.pop(task, None)
self._level += amount
return None
async def __aenter__(self) -> None:
await self.acquire()
return None
async def __aexit__(
self,
exc_type: Optional[Type[BaseException]],
exc: Optional[BaseException],
tb: Optional[TracebackType]
) -> None:
return None
请注意,我们是有机会从存储桶中泄漏容量,因此无需运行单独的异步任务即可降低该级别;而是在测试足够的剩余容量时泄漏了容量。
请注意,等待容量的任务保存在有序字典中,并且当可能有容量再次空闲时,第一个仍在等待的任务会提前唤醒。
您可以将其用作上下文管理器;尝试在存储块已满时获取存储桶,直到再次释放足够的容量为止:
bucket = AsyncLeakyBucket(100)
# ...
async with bucket:
# only reached once the bucket is no longer full
或者您可以acquire()
直接致电:
await bucket.acquire() # blocks until there is space in the bucket
或者您可以简单地先测试是否有空间:
if bucket.has_capacity():
# reject a request due to rate limiting
请注意,您可以通过增加或减少“滴入”存储桶的数量来将某些请求视为“较重”或“较轻”:
await bucket.acquire(10)
if bucket.has_capacity(0.5):
不过要小心点;当混合大小液滴时,在达到或接近最大速率时,小液滴往往先于大液滴流失,因为存在较大可能性的可能性是,在有较大液滴空间之前,有足够的自由容量用于较小液滴。
演示:
>>> import asyncio, time
>>> bucket = AsyncLeakyBucket(5, 10)
>>> async def task(id):
... await asyncio.sleep(id * 0.01)
... async with bucket:
... print(f'{id:>2d}: Drip! {time.time() - ref:>5.2f}')
...
>>> ref = time.time()
>>> tasks = [task(i) for i in range(15)]
>>> result = asyncio.run(asyncio.wait(tasks))
0: Drip! 0.00
1: Drip! 0.02
2: Drip! 0.02
3: Drip! 0.03
4: Drip! 0.04
5: Drip! 2.05
6: Drip! 4.06
7: Drip! 6.06
8: Drip! 8.06
9: Drip! 10.07
10: Drip! 12.07
11: Drip! 14.08
12: Drip! 16.08
13: Drip! 18.08
14: Drip! 20.09
开始时,存储桶会突然装满,导致其余任务更均匀地分配;每2秒释放出足够的容量以处理另一个任务。
在上面的演示中,最大突发大小等于最大速率值,该示例已将其设置为5。如果您不想允许突发,请将最大速率设置为1,并将时间间隔设置为两次滴水之间的最小时间:
>>> bucket = AsyncLeakyBucket(1, 1.5) # no bursts, drip every 1.5 seconds
>>> async def task():
... async with bucket:
... print(f'Drip! {time.time() - ref:>5.2f}')
...
>>> ref = time.time()
>>> tasks = [task() for _ in range(5)]
>>> result = asyncio.run(asyncio.wait(tasks))
Drip! 0.00
Drip! 1.50
Drip! 3.01
Drip! 4.51
Drip! 6.02
我已经将其打包为Python项目:https :
//github.com/mjpieters/aiolimiter
问题内容: 我写了这段代码 然后我尝试在另一个文件中使用它 我有一个错误 “等待仅在异步功能中有效” 有什么问题 问题答案: 错误不是指而是。 我利用这个问题的机会来告诉你一个已知的反模式的使用方法:。 错误 正确 另外,要知道有一个正确且重要的特殊情况:(使用try / catch)
问题内容: 最近,我不得不更正Web应用程序(我没有创建)中的安全性问题。安全问题是,它正在使用非仅限HTTP的cookie。因此,我不得不将session- cookie设置为仅http-,这意味着您无法再从javascript中读取(设置)cookie的值。到目前为止,接缝都很容易。 更深层的问题是,使用的Web应用程序 在一百万个地方 。 因此,为了不必重写“一百万行代码”,我不得不创建一个
本文向大家介绍spring异步service中处理线程数限制详解,包括了spring异步service中处理线程数限制详解的使用技巧和注意事项,需要的朋友参考一下 情况简介 spring项目,controller异步调用service的方法,产生大量并发。 具体业务: 前台同时传入大量待翻译的单词,后台业务接收单词,并调用百度翻译接口翻译接收单词并将翻译结果保存到数据库,前台不需要实时返回翻译结果
我正在尝试做一个简单的应用程序,加载数据并对其执行一个操作.所以我的想法是做这个异步。 我有3个数据源,我想异步加载它们。例如data1.xml、data2.xml和data3.xml所有文件加载起来都相当大,所以需要一些时间(这就是为什么我想要异步的原因)。 例如,我创建了一个包含3个文本框的窗口,这些文本框都绑定到一个特定的属性(Text1、Text2、Text3)和一个按钮。当我点击按钮时,
场景:我需要从web应用程序的API收集分页数据,该API的调用限制为每分钟100次。我需要返回的API对象每页包含100个项目,总共105个页面,并且在不断增长(~10500个项目)。同步代码检索所有页面大约需要15分钟,因此不必担心达到调用限制。但是,我想加快数据检索速度,因此我使用和实现了异步调用。数据现在可以在15秒内下载-很好。 问题:我现在达到呼叫限制,因此在过去的5个左右的呼叫中收到
我正在做一个函数(Java-Android),它应该返回一个由Firebase中的数据填充的对象列表。我的问题是,我需要在遍历for中的所有项目后将结果发送给监听器,看看代码: 我需要执行这一行: