python 等待 API 调用时的多线程

python multithreading while waiting for API call

详细说明:我正在使用 python 制作一个小应用程序,它可以抓取前 100 首歌曲列表并从中创建一个 spotify 播放列表。我的瓶颈是 spotify API 一次只能搜索一首歌曲(以获取其内部 spotify ID)。

简而言之:我尝试了混合结果的多线程。

供参考,这是搜索歌曲的作用,不完全相关:

    def __search_song(self, song: str):
    result = self.sp.search(song + " NOT Karaoke", limit=1, type="track")
    try:
        sid = result["tracks"]["items"][0]["uri"]
    except IndexError:
        pass
    else:
        self.song_list.append(sid)

初步实施:

def __populate_playlist(self, song_list: list, pid: str):
    for song in song_list:
        self.__search_song(song)

    self.sp.playlist_add_items(pid, self.song_list)

这是正常执行,“一个接一个”,它运行良好,但速度很慢,并且由于 UI(Tkinter 需要不断刷新)而使 window 挂起。

使用线程和队列的多线程:

q = queue.Queue()


def __worker():
    while True:
        item = q.get()
        q.task_done()


threading.Thread(target=__worker, daemon=True).start()

def __populate_playlist(self, song_list: list, pid: str):
    for song in song_list:
        q.put(self.__search_song(song))

    q.join()
    self.sp.playlist_add_items(pid, self.song_list)

这有效,但是比原来的速度稍快。它确实解决了程序似乎没有响应的问题,但速度不够快。

然后我尝试删除队列并实现无序线程。

def __populate_playlist(self, song_list: list, pid: str):
    # multiprocessing support
    threads = []
    for song in song_list:
        t = threading.Thread(target=self.__search_song, args=(song, ))
        threads.append(t)

    for thread in threads:
        thread.start()

    for thread in threads:
        thread.join()

    self.sp.playlist_add_items(pid, self.song_list)

这非常快,我说的是从 23 秒减少到 8 秒。显然,这会产生意想不到的后果,即播放列表被打乱,不再是真正的前 100 名。

我的问题很简单,是我的队列实现有问题,还是使用队列系统本身就提供了这么多开销?这是我第一次在应用程序中实现多线程,所以我可能遗漏了一些东西。

要再次遍历用例,我真的不在乎哪个先完成,只要保持顺序即可。我想过存储列表的初始顺序并使用字典来保存它的顺序和 spotify ID,但我仍在考虑它的实际实现。

如前所述,如果您想要异步调用,则很难保证顺序。但是将 ID 映射到歌曲名称的简单实现是:

def __search_song(self, song: str):

    result = self.sp.search(song + " NOT Karaoke", limit=1, type="track")
    
    try:
        sid = result["tracks"]["items"][0]["uri"]
    except IndexError:
        pass
    else:
        self.song_list.append(sid)
        self.song_to_sid[song] = sid

鉴于字典 song_to_sid 已在您 class 中实例化。 如果您随后只是遍历您的第一张地图(如果按顺序排列),您可以附加映射的 sid 以获得有序的播放列表。

拥有 运行 __populate_playlist 功能后,您可以执行以下操作:

top_hundred_playlist = []
for song_id in self.song_list:
    top_hundred_playlist(self.song_to_sid[song_id])

在 Albin Sidås 的帮助下,最终代码看起来像这样:

def __search_song(self, song: str):
    """searches a song by a string song name returns spotify URI id"""
    result = self.sp.search(song + " NOT Karaoke", limit=1, type="track")
    try:
        sid = result["tracks"]["items"][0]["uri"]
    except IndexError:
        self.song_to_sid[song] = ""
    else:
        self.song_to_sid[song] = sid

def __populate_playlist(self, song_list: list, pid: str):
    # multiprocessing support
    top_hundred_ids = []
    threads = []
    for song in song_list:
        self.song_list_names.append(song)
        t = threading.Thread(target=self.__search_song, args=(song, ))
        threads.append(t)

    for thread in threads:
        thread.start()

    for thread in threads:
        thread.join()

    for song_id in self.song_list_names:
        song_value = self.song_to_sid[song_id]
        if song_value != "":
            top_hundred_ids.append(song_value)

    self.sp.playlist_add_items(pid, top_hundred_ids)

它最终只比完全异步解决方案慢了一秒钟,所以我认为这是成功的方法。我仍然愿意接受任何关于队列系统开销的澄清,但总而言之,这很棒。