为什么Queue.join()需要在这里?

时间:2017-03-02 14:59:41

标签: python python-multithreading

我正在学习python的线程模块,并编写了以下代码以帮助自己理解

from Queue import Queue
import threading

lock = threading.Lock()
MAX_THREADS = 8
q = Queue()
count = 0

# some i/o process
def io_process(x):
    pass

# process that deals with shared resources
def shared_resource_process(x):
    pass

def func():
    global q, count
    while not q.empty():
        x = q.get()
        io_process(x)
        if lock.acquire():
            shared_resource_process(x)
            print '%s is processing %r' %(threading.currentThread().getName(), x)
            count += 1          
            lock.release()

def main():
    global q
    for i in range(40):
        q.put(i)

    threads = []
    for i in range(MAX_THREADS):
        threads.append(threading.Thread(target=func))

    for t in threads:
        t.start()

    for t in threads:
        t.join()

    print 'multi-thread done.'
    print count == 40

if __name__ == '__main__':
    main()

并且输出卡住了这样:

Thread-1 is processing 32
Thread-8 is processing 33
Thread-6 is processing 34
Thread-2 is processing 35
Thread-5 is processing 36
Thread-3 is processing 37
Thread-7 is processing 38
Thread-4 is processing 39

请注意,main()中的打印不会被执行,这意味着某些线程挂起/阻塞?

然后我通过添加q.task_done()来修改func()方法:

if lock.acquire():
            shared_resource_process(x)
            print '%s is processing %r' %(threading.currentThread().getName(), x)
            count += 1
            q.task_done()  # why is this necessary ?
            lock.release()

现在所有线程都按照我的预期终止并获得正确的输出:

Thread-6 is processing 36
Thread-4 is processing 37
Thread-3 is processing 38
Thread-7 is processing 39
multi-thread done.
True

Process finished with exit code 0

我阅读了Queue.Queue here的文档,并看到task_done()与queue.join()配合使用,以确保处理队列中的所有项目。但是因为我没有在main()中调用queue.join(),为什么task_done()必须在func()中?当我错过task_done()代码时,线程挂起/阻塞的原因是什么?

1 个答案:

答案 0 :(得分:3)

您的代码中存在竞争条件。想象一下,Queue中只剩下一个项目而你只使用两个线程而不是8个。然后发生以下事件序列:

  1. 主题A调用q.empty来检查它是否为空。由于队列中有一个项目结果是False并且循环体被执行。
  2. 在线程A调用q.get之前,有一个上下文切换,线程B可以运行。
  3. 线程B调用q.empty,队列中仍有一个项目,因此结果为False并执行循环体。
  4. 线程B在没有参数的情况下调用q.get,并立即返回队列中的最后一项。然后线程B处理该项并退出,因为q.empty返回True
  5. 线程A开始运行。由于它已在步骤1中调用q.empty,因此它将在下一次调用q.get,但这将永久阻止,因此您的程序将不会终止。
  6. 您可以通过导入time并稍微更改循环来模拟上述行为:

    while not q.empty():
        time.sleep(0.1) # Force context switch
        x = q.get()
    

    请注意,无论是否调用task_done,行为都是相同的。

    那为什么添加task_done有帮助?默认情况下,Python 2将每100个解释器指令执行上下文切换,因此添加代码可能会更改上下文切换发生的位置。有关更好的说明,请参阅another questionlinked PDF。在我的机器上,无论task_done是否存在,程序都没有挂起,所以这只是一个猜测导致它发生的原因。

    如果你想修复这个行为,你可能只有无限循环并将​​参数传递给get指示它不会阻塞。这会导致get最终抛出您可以捕获的Queue.Empty异常,然后打破循环:

    from Queue import Queue, Empty
    
    def func():
        global q, count
        while True:
            try:
                x = q.get(False)
            except Empty:
                break
            io_process(x)
            if lock.acquire():
                shared_resource_process(x)
                print '%s is processing %r' %(threading.currentThread().getName(), x)
                count += 1
                lock.release()