Python3中,如何实现非阻塞的队列先后读取?

目测我的标题取错了,但是我实在不知道怎么用术语描述。

问题大概是这样的:在用 python 写一个五子棋 AI,那么就需要玩家下一步,电脑思考,电脑下一步。现在电脑思考大概需要 2 秒的时间。

代码大概是这样的:

que_game2ui = Queue(maxsize=2)
que_ui2game = Queue(maxsize=2)

class GUI: def init(self, ui): # self.bg = ui 的一个 canvas pass

def on_click(self, event):
    col = (event.x + GUICONF["half_gap"]) // GUICONF["gap"] - 1
    row = (event.y + GUICONF["half_gap"]) // GUICONF["gap"] - 1
    que_ui2game.put({
        "option": "move",
        "loc": (row, col),
    })


def queue_handler(self):
try:
    task = que_game2ui.get(block=False)
    # 更新界面
    self.bg.after(10, self.queue_handler)
except queue.Empty:
    self.bg.after(10, self.queue_handler)

class GAME(Gomokuy): def init(self, _gui) # Gomokuy 是自己写的 AI 的类 # self.gui = _gui pass

def moving(self, row, col):
    if self.winner:
        print("游戏结束")
        return

    ret = self.move((row, col))
    if ret:
        que_game2ui.put({
            "game": self,
            "info": "move"}, block=False)

        pos2 = self.iterative_deepening(self.difficulty)	# 这个函数很耗时
        self.move(pos2)
        que_game2ui.put({
            "game": self,
            "info": "move"}, block=False)
    else:
        que_game2ui.put({
            "game": self,
            "info": "Game Over"})


def queue_handler(self):
    try:
        task = que_ui2game.get(block=False)
        if task["option"] == "move":
            row, col = task["loc"]
            self.moving(row, col)
        self.gui.after(10, self.queue_handler)
    except queue.Empty:
        self.gui.after(10, self.queue_handler)

if name == ‘main’: window = Tk() gui = GUI(window) t1 = threading.Thread(target=GAME, args=(window,)) t1.setDaemon = True t1.start() window.mainloop()

问题在于 GAME.moving 这里,分别有两次 put 操作,我设想的情况应该是:

  1. 第一次 put 之后,GAME 线程继续计算下一步 AI 应该怎么走( iterative_deepening )。
  2. GUI.queue_handler 拿到第一次 put 的内容之后,立即把玩家下的这一步绘制出来。
  3. n 秒后,GAME 算出来了,于是第二次 put,然后 GUI 再绘制,把电脑的落子也绘制出来。

但是根据实际症状,以及添加的调试信息来看,结果实际是这样的:

  1. 第一次 put 之后,GAME 算,算完之后才第二次 put。
  2. 第二次 put 之后,GUI 一次性把两次信息都拿出来(这个还看不出来,似乎 GUI 每次都只取到了最后一次 put,也就是走了两步之后的结果)绘制。
  3. 症状就是,点击之后没有任何反馈,然后一瞬间走两步。

感觉没有达到拆分线程的目的,请问代码思路是哪里有问题呢?


Python3中,如何实现非阻塞的队列先后读取?

3 回复

在Python 3里搞非阻塞的队列读取,直接用queue.Queueget_nowait()方法就行。这玩意儿在队列空的时候会立刻抛queue.Empty异常,不会卡住你的程序。

下面是个简单例子,一个线程往里塞数据,主线程非阻塞地取:

import queue
import threading
import time

def producer(q):
    """生产者线程,每秒塞一个数"""
    for i in range(5):
        q.put(i)
        print(f'生产了: {i}')
        time.sleep(1)

def main():
    q = queue.Queue()
    
    # 启动生产者线程
    prod_thread = threading.Thread(target=producer, args=(q,))
    prod_thread.start()
    
    # 主线程非阻塞读取
    items_received = 0
    while items_received < 5:
        try:
            item = q.get_nowait()  # 关键在这,非阻塞获取
            print(f'消费了: {item}')
            items_received += 1
        except queue.Empty:
            print('队列是空的,干点别的...')
            time.sleep(0.5)  # 模拟其他操作
    
    prod_thread.join()
    print("搞定!")

if __name__ == "__main__":
    main()

跑起来你会看到,主线程在队列空的时候不会傻等,而是执行except块里的逻辑。等生产者放了东西,get_nowait()就能立刻拿到。

如果你在用asyncio,那就用asyncio.Queueget_nowait(),用法差不多,但得配合async/await

简单说就是:用get_nowait()别用get()


原因很简单,你的queue_handler都是通过 Tkinter 组件的after方法调用的,这个方法调用queue_handler方法是在主线程( UI 线程中),也就是说iterative_deepening阻塞了 UI 线程,所以卡住了


谢谢!我试试看。

回到顶部