Python非Daemon线程调用栈持续运行问题排查
非守护线程持续运行导致进程无法终止的原因分析
问题场景
我编写了一段用队列管理任务的多线程图片下载代码,调试时确认队列已为空,my_queue.join()执行后主线程正常输出“All done!”,但非Daemon线程仍在调用栈中持续运行,进程无法结束。将线程设置为threading.Thread(target=worker, args=(my_queue,), daemon=True).start()后,进程可正常终止,想知道非守护线程持续运行的原因。
代码如下:
import queue from urllib.request import urlopen, Request import os import threading HEADER = {'User-Agent': 'Mozilla/5.0'} # Note: only URL:s ending on .jpg works IMG_URLS = [ "https://cdn.pixabay.com/photo/2017/03/08/13/06/dog-2126708__340.jpg", "https://cdn.pixabay.com/photo/2016/06/05/22/45/otter-1438381__340.jpg", "https://cdn.pixabay.com/photo/2014/10/22/14/43/otter-498046__340.jpg", "https://cdn.pixabay.com/photo/2017/10/23/23/43/otter-2883047__340.jpg" ] # Change to desired folder SAVE_FOLDER = r"C:\Users\xxxxxxxx" my_queue = queue.Queue() def generate_queue(list): for url in list: my_queue.put(url) def create_and_run_threads(): number_threads=2 for _ in range(number_threads): threading.Thread(target=worker, args=(my_queue,)).start() def worker(q): while q: url=q.get() download_img(url) my_queue.task_done() def download_img(url): img_bytes = request_and_open(url) img_name = url.split("/")[-1] file_path = os.path.join(SAVE_FOLDER, img_name) with open(file_path, "wb") as img: img.write(img_bytes) print(f"Downloading image: {img_name}") def request_and_open(URL): request = Request(URL, headers=HEADER) url_info_byte = urlopen(request, timeout=20).read() return url_info_byte def main(): generate_queue(IMG_URLS) create_and_run_threads() my_queue.join() print("All done!") if __name__ == "__main__": main()
原因分析
问题核心出在worker函数的循环条件和队列的阻塞特性上:
- 循环条件
while q永远为True:这里的q是队列实例对象,只要这个对象没有被销毁,判断结果就一直是真,线程不会主动退出循环。 q.get()默认是阻塞式调用:当队列中的所有任务都处理完毕后,线程会卡在url=q.get()这一行,一直等待新的任务被放入队列,不会自己停止。
主线程执行my_queue.join()只是确认所有已提交的任务都处理完成,但不会通知工作线程停止等待。此时非守护线程还在阻塞等待队列的新任务,而Python进程会等待所有非守护线程结束后才会终止,所以进程无法正常退出。
设置daemon=True后,主线程结束时会强制终止所有守护线程,进程也就随之正常结束了。
修复方案
可以通过两种方式让工作线程主动退出:
- 给
q.get()设置超时,捕获空队列异常后退出:
def worker(q): while True: try: # 设置超时时间,避免永久阻塞 url = q.get(timeout=1) download_img(url) q.task_done() except queue.Empty: # 队列为空且超时,退出循环 break
- 主线程在任务完成后发送终止标记,让线程主动退出:
def main(): generate_queue(IMG_URLS) number_threads = 2 create_and_run_threads() my_queue.join() # 给每个工作线程发送终止标记 for _ in range(number_threads): my_queue.put(None) print("All done!") def worker(q): while True: url = q.get() if url is None: # 收到终止标记,退出线程 q.task_done() break download_img(url) q.task_done()
内容的提问来源于stack exchange,提问作者EstherPM
相关产品推荐
相关产品推荐

