C#程序员试图学习一些Python。我正在尝试运行CPU密集型计算,同时让IO绑定的异步方法在后台悄悄切换。在C#中,我通常会设置等待的时间,然后启动CPU密集型代码,然后等待IO任务,然后合并结果。
这就是我在C#中要做的事情
static async Task DoStuff() {
var ioBoundTask = DoIoBoundWorkAsync();
int cpuBoundResult = DoCpuIntensizeCalc();
int ioBoundResult = await ioBoundTask.ConfigureAwait(false);
Console.WriteLine($"The result is {cpuBoundResult + ioBoundResult}");
}
static async Task<int> DoIoBoundWorkAsync() {
Console.WriteLine("Make API call...");
await Task.Delay(2500).ConfigureAwait(false); // non-blocking async call
Console.WriteLine("Data back.");
return 1;
}
static int DoCpuIntensizeCalc() {
Console.WriteLine("Do smart calc...");
Thread.Sleep(2000); // blocking call. e.g. a spinning loop
Console.WriteLine("Calc finished.");
return 2;
}
这是python中的等效代码
import time
import asyncio
async def do_stuff():
ioBoundTask = do_iobound_work_async()
cpuBoundResult = do_cpu_intensive_calc()
ioBoundResult = await ioBoundTask
print(f"The result is {cpuBoundResult + ioBoundResult}")
async def do_iobound_work_async():
print("Make API call...")
await asyncio.sleep(2.5) # non-blocking async call
print("Data back.")
return 1
def do_cpu_intensive_calc():
print("Do smart calc...")
time.sleep(2) # blocking call. e.g. a spinning loop
print("Calc finished.")
return 2
await do_stuff()
重要的是,请注意,CPU密集型任务由无法等待的阻塞睡眠表示,与IO绑定的任务由等待的非阻塞睡眠表示。
在C#中运行需要2.5秒,在Python中运行需要4.5秒。区别在于C#立即运行异步方法,而python仅在等待时启动该方法。以下输出确认了这一点。我怎样才能达到预期的效果。如果可能的话,将欢迎在Jupyter Notebook中工作的代码。
--- C# ---
Make API call...
Do smart calc...
Calc finished.
Data back.
The result is 3
--- Python ---
Do smart calc...
Calc finished.
Make API call...
Data back.
The result is 3
受knh190答案的启发,似乎我可以使用asyncio.create_task(...)
来达到大部分目的。这样可以达到预期的结果(2.5秒):首先,异步代码设置为运行;接下来,阻塞的CPU代码将同步运行;第三,等待异步代码;最后将结果合并。为了使异步调用真正开始运行,我必须放入一个await asyncio.sleep(0)
,感觉就像是一个骇人的骇客。我们可以不执行此操作而设置任务运行吗?一定有更好的方法...
async def do_stuff():
task = asyncio.create_task(do_iobound_work_async())
await asyncio.sleep(0) # <~~~~~~~~~ This hacky line sets the task running
cpuBoundResult = do_cpu_intensive_calc()
ioBoundResult = await task
print(f"The result is {cpuBoundResult + ioBoundResult}")
答案 0 :(得分:2)
我认为您的测试是不言而喻的。 Python中await
和async
的前身是生成器(in Python 2)。 Python只会创建一个协程,而不会在您显式调用它之前启动它。
因此,如果您想像C#一样立即触发协同程序,则需要将await
行向前移动。
async def do_stuff():
ioBoundTask = do_iobound_work_async() # created a coroutine
ioBoundResult = await ioBoundTask # start the coroutine
cpuBoundResult = do_cpu_intensive_calc()
print(f"The result is {cpuBoundResult + ioBoundResult}")
这等效于:
def do_stuff():
# create a generator based coroutine
# cannot mix syntax of asyncio
ioBoundTask = do_iobound_work_async()
ioBoundResult = yield from ioBoundTask
# whatever
另请参阅此帖子:In practice, what are the main uses for the new "yield from" syntax in Python 3.3?
我注意到您的C#和Python不是严格等效的。 Python中只有asyncio.Task是并发的:
async def do_cpu_intensive_calc():
print("Do smart calc...")
await asyncio.sleep(2)
print("Calc finished.")
return 2
# 2.5s
async def do_stuff():
task1 = asyncio.create_task(do_iobound_work_async())
task2 = asyncio.create_task(do_cpu_intensive_calc())
ioBoundResult = await task1
cpuBoundResult = await task2
print(f"The result is {cpuBoundResult + ioBoundResult}")
现在执行时间应该相同。
答案 1 :(得分:0)
因此,通过更多的研究,似乎可以实现,但并不像C#那样容易。 do_stuff()
的代码变为:
async def do_stuff():
task = asyncio.create_task(do_iobound_work_async()) # add task to event loop
await asyncio.sleep(0) # return control to loop so task can start
cpuBoundResult = do_cpu_intensive_calc() # run blocking code synchronously
ioBoundResult = await task # at last, we can await our async code
print(f"The result is {cpuBoundResult + ioBoundResult}")
与C#相比,两个区别是:
asyncio.create_task(...)
将任务添加到正在运行的事件循环中需要await asyncio.sleep(0)
暂时将控制权返回到事件循环,以便它可以启动任务。现在完整的代码示例为:
import time
import asyncio
async def do_stuff():
task = asyncio.create_task(do_iobound_work_async()) # add task to event loop
await asyncio.sleep(0) # return control to loop so task can start
cpuBoundResult = do_cpu_intensive_calc() # run blocking code synchronously
ioBoundResult = await task # at last, we can await our async code
print(f"The result is {cpuBoundResult + ioBoundResult}")
async def do_iobound_work_async():
print("Make API call...")
await asyncio.sleep(2.5) # non-blocking async call. Hence the use of asyncio
print("Data back.")
return 1
def do_cpu_intensive_calc():
print("Do smart calc...")
time.sleep(2) # long blocking code that cannot be awaited. e.g. a spinning loop
print("Calc finished.")
return 2
await do_stuff()
我不太喜欢必须记住添加额外的await asyncio.sleep(0)
才能开始任务。拥有begin_task(...)
之类的等待功能来自动启动任务,以便稍后可以等待它,可能会更整洁。例如,如下所示:
async def begin_task(coro):
"""Awaitable function that adds a coroutine to the event loop and sets it running."""
task = asyncio.create_task(coro)
await asyncio.sleep(0)
return task
async def do_stuff():
io_task = await begin_task(do_iobound_work_async())
cpuBoundResult = do_cpu_intensive_calc()
ioBoundResult = await io_task
print(f"The result is {cpuBoundResult + ioBoundResult}")