背景: 我正在开发一个使用请求的 Discord 机器人。请求是异步的,所以我使用库asgiref.sync。
(我知道我显然不能将此函数用于异步函数。)
我在所有请求和可能需要很长时间处理的事情中实现了 sync_to_async。该函数不会产生任何错误。不过,我不确定这是否真的有什么作用。
我可以对 Python 中的任何函数使用sync_to_async吗?
是的,但是...
这是否意味着我可以将任何函数转换为异步函数,以使事情运行得更快、更有凝聚力(...)?
不。这是一个代码示例:
import asyncio
import time
from asgiref.sync import async_to_sync, sync_to_async
def sleep(t0):
time.sleep(1)
print(time.time() - t0)
async def async_sleep(t0):
await asyncio.sleep(1)
print(time.time() - t0)
sync_to_async_sleep = sync_to_async(sleep)
async_to_sync_sleep = async_to_sync(async_sleep)
async def async_main():
print('sync')
t0 = time.time()
sleep(t0)
sleep(t0)
print('\nsync_to_async')
t0 = time.time()
await asyncio.gather(
sync_to_async_sleep(t0),
sync_to_async_sleep(t0),
)
print('\nasync')
t0 = time.time()
await asyncio.gather(
async_sleep(t0),
async_sleep(t0),
)
def main():
print('\nasync_to_sync')
t0 = time.time()
async_to_sync_sleep(t0)
async_to_sync_sleep(t0)
asyncio.run(async_main())
main()
输出:
sync
1.00
2.01
sync_to_async
1.00
2.01
async
1.00
1.00
async_to_sync
1.00
2.01
显然,只有“异步”情况(异步上下文中的真正异步实现)同时运行。
异步并不会神奇地让你的程序并行运行。在异步函数中,需要在某些点上函数“放弃控制”,例如等待远程服务器的响应时。
如果您将函数“转换”为异步(1)CPU 限制或(2)不通过在某处调用
await
语句来放弃控制,或(3)两者,则该函数将运行直到完成为止.
换句话说:异步是协作式多任务处理。异步函数必须在某些点“移交”控制权给异步循环,以允许其他函数运行。
如果没有看到你的代码,就很难知道你是否获得了任何好处或者你可以在哪些方面做得更好。
但是,你可以使用
sync_to_async
在不同线程上并发执行同步函数,但是你必须通过thread_sensitive=False
。因此,只需用 sync_to_async
位来修改 aaron 的答案:
import asyncio
import time
from asgiref.sync import sync_to_async
@sync_to_async(thread_sensitive=False)
def async_sleep(t0):
time.sleep(1)
print(time.time() - t0)
async def async_main():
t0 = time.time()
await asyncio.gather(
async_sleep(t0),
async_sleep(t0),
)
asyncio.run(async_main())
输出:
1.0007977485656738
1.0009398460388184
但是要小心,根据 asgiref 自述文件:
请注意,运行的线程是非常具体的[...snip...] 默认情况下,出于安全原因,sync_to_async 将在同一线程中运行程序中的所有同步代码; 您可以使用
禁用此功能...,但请确保您的代码不依赖于任何绑定到线程的内容(例如数据库连接)。@sync_to_async(thread_sensitive=False)
(强调我的。)
因此,在 @aaron 的示例中,它不起作用的原因是因为两个同步函数在同一线程上相继运行。为了获得并发性,你只需添加
thread_sensitive=False
,现在两个同步函数调用在单独的线程上并行运行(或者至少按照 GIL 允许的并行程度),但现在你有责任确保它们可以处理并行运行并且不会互相踩踏。
您可能还想知道,如果不设置
sync_to_async
,thread_sensitive=False
的意义何在。在这个例子中没有,因为同步函数被多次分派,并且希望它们能够同时运行,但事实并非如此。但它确实允许您与异步函数并行运行单个同步函数,如下所示:
import asyncio
import time
from asgiref.sync import sync_to_async
@sync_to_async
def async_sleep(t0):
time.sleep(1)
print(time.time() - t0)
async def real_async_sleep(t0):
await asyncio.sleep(1)
print(time.time() - t0)
async def async_main():
t0 = time.time()
await asyncio.gather(
async_sleep(t0),
real_async_sleep(t0),
)
asyncio.run(async_main())
输出:
1.0015418529510498
1.0017149448394775
不要使用 (thread_sensitive=False),只需在 settings.py 中添加 os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" 即可。
#views.py
from django.http import HttpResponse
import time
from .models import Movies, Theatres
from asgiref.sync import sync_to_async
import asyncio
#helper functions
def get_movies():
print("getting movies ....")
time.sleep(8)
qs = Movies.objects.all()
print(qs)
print("all movies fetched")
def get_theatres():
print("getting theatres ...")
time.sleep(3)
qs = Theatres.objects.all()
print(qs)
print("all theatres fetched")
async def get_movies_async():
print("getting movies ....")
await asyncio.sleep(8)
qs = Movies.objects.all()
print(qs)
print("all movies fetched")
async def get_theatres_async():
print("getting theatres ...")
await asyncio.sleep(3)
qs = Theatres.objects.all()
print(qs)
print("all theatres fetched")
#Main Views
def sync_view(request):
start_time = time.time()
get_movies()
get_theatres()
total = time.time()-start_time
return HttpResponse(f"time taken {total}")
async def async_view(request):
start_time = time.time()
await asyncio.gather(get_movies_async(), get_theatres_async())
total = time.time()-start_time
return HttpResponse(f"time taken async {total}")