当我明确指出我只希望第一个任务完成时,为什么所有任务都在asyncio.wait()中完成?

时间:2019-04-14 08:35:18

标签: python-3.x asynchronous python-asyncio

我正在尝试使用asyncio从OpenSubtitles中获取一些数据,然后下载该数据中包含信息的文件。我想获取这些数据并使用asyncio同时下载文件。

问题是我要等待列表tasks中的1个任务完成,然后再开始列表或download_tasks中的其余任务。这样做的原因是,在self._perform_query()中,我正在向文件中写入信息,而在self._download_and_save_file()中,我正在从该文件中读取相同的信息。因此,换句话说,download_tasks需要等待tasks中的至少一项任务完成才能开始。

我发现我可以使用asyncio.wait(return_when=FIRST_COMPLETED)来做到这一点,但是由于某种原因,它无法正常工作:

payloads = [create_payloads(entry) for entry in retreive(table_in_database)] 
tasks = [asyncio.create_task(self._perform_query(payload, proxy)) for payload in payloads]
download_tasks = [asyncio.create_task(self._download_and_save_file(url, proxy) for url in url_list]

done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
print(done)
print(len(done))
print(pending)
print(len(pending))
await asyncio.wait(download_tasks)

输出与预期完全不同。尽管我通过了tasks,列表asyncio.FIRST_COMPLETED中的3个任务似乎都已经完成了。为什么会这样?

{<Task finished coro=<SubtitleDownloader._perform_query() done, defined at C:\Users\...\subtitles.py:71> result=None>, <Task finished coro=<SubtitleDownloader._perform_query() done, defined at C:\Users\...\subtitles.py:71> result=None>, <Task finished coro=<SubtitleDownloader._perform_query() done, defined at C:\Users\...\subtitles.py:71> result=None>}
3
set()
0
Exiting

据我所知,self._perform_query()中的代码不应影响此问题。无论如何,这里只是要确保:

async def _perform_query(self, payload, proxy):
    try:
        query_result = proxy.SearchSubtitles(self.opensubs_token, [payload], {"limit": 25})
    except Fault as e:
        raise "A fault has occurred:\n{}".format(e)
    except ProtocolError as e:
        raise "A ProtocolError has occurred:\n{}".format(e)
    else:
        if query_result["status"] == "200 OK":
            with open("dl_links.json", "w") as dl_links_json:
                result = query_result["data"][0]
                subtitle_name = result["SubFileName"]
                download_link = result["SubDownloadLink"]
                download_data = {"download link": download_link,
                                 "file name": subtitle_name}
                json.dump(download_data, dl_links_json)
        else:
            print("Wrong status code: {}".format(query_result["status"]))

就目前而言,我一直在不运行download_tasks的情况下对其进行测试,但出于上下文的考虑,在此进行了介绍。也许我正在以完全错误的方式来解决这个问题。如果是这样,非常感谢您的投入!

编辑:

问题非常简单,如下所述。 _perform_query不是一个等待的函数,而是同步运行的。我通过将_perform_query的文件写入部分编辑为与aiofiles异步来改变了这一点:

def _perform_query(self, payload, proxy):
    query_result = proxy.SearchSubtitles(self.opensubs_token, [payload], {"limit": 25})
    if query_result["status"] == "200 OK":
        async with aiofiles.open("dl_links.json", mode="w") as dl_links_json:
            result = query_result["data"][0]
            download_link = result["SubDownloadLink"]
            await dl_links_json.write(download_link)

1 个答案:

答案 0 :(得分:0)

return_when=FIRST_COMPLETED不保证只完成一个任务。它保证等待将在任务完成后立即完成,但是其他任务很有可能“同时”完成,对于异步而言,这意味着在事件循环的同一迭代中进行。例如,考虑以下代码:

async def noop():
    pass

async def main():
    done, pending = await asyncio.wait(
        [noop(), noop(), noop()], return_when=asyncio.FIRST_COMPLETED)
    print(len(done), len(pending))

asyncio.run(main())

这将打印3 0,就像您的代码一样。为什么?

asyncio.wait做两件事:将协程提交到事件循环,并设置回调以在完成任何回调时通知它。但是,noop协程不包含await,因此对noop()的调用都不会挂起,每个都只是做自己的事情并立即返回。结果,所有三个协程实例在事件循环的同一遍内完成。然后wait被告知所有三个协程均已完成,并尽职尽责地报告了这一事实。

如果您更改noop以等待随机睡眠,例如将pass更改为await asyncio.sleep(0.1 * random.random()),您将获得预期的行为。使用await时,协程不再同时完成,wait将在检测到第一个后立即报告。

这揭示了您的代码的真正潜在问题:_perform_query 不用等待。这表明您没有使用异步基础库,或者使用不正确。对SearchSubtitles的调用可能只是阻塞了事件循环,该事件循环在简单的测试中似乎可以正常工作,但破坏了诸如同步执行任务之类的基本异步功能。