如何在Python 3中创建启动任意数量线程?多线程图片下载报错求助
嘿,我来帮你理清楚这个问题哈!
首先,你遇到的TypeError: downloading() takes 1 positional arguments but 21 were given,核心原因是线程参数传递的语法坑:
当你用Thread(target=downloading, args=(queue))时,Python会把queue这个集合当成可迭代对象直接解包——因为集合里有21个URL,相当于给downloading函数传了21个参数,但你的函数只定义了1个参数,所以直接报错了。
正确的单参数传递方式是给元组加个逗号:args=(queue,),这样Python才会把整个集合作为单个参数传给函数,而不是拆成一堆参数。
不过,就算你修复了这个语法问题,当前的代码还有两个大问题:线程安全风险和效率低下,我来给你优化一下:
为啥你的代码还有坑?
- 线程不安全:全局变量
count和集合done在多线程同时读写时,会出现数据竞争——比如两个线程同时检查同一个URL是否在done里,结果都认为不在,导致重复下载;计数count也会因为同时修改出现错误。 - 效率低:每个线程都要遍历整个21个URL的集合,大部分时间都在做“是否已下载”的检查,完全是浪费资源。
优化后的靠谱版本
用Python标准库的queue.Queue来做任务队列(天生线程安全),再用Lock保证计数的安全性,代码更简洁也更靠谱:
import urllib.request from threading import Thread, Lock from queue import Queue # 线程安全的计数器和锁 download_count = 0 count_lock = Lock() save_path = 'foldername' def download_worker(task_queue, thread_name): global download_count while not task_queue.empty(): img_url = task_queue.get() # 从队列取任务,空队列时自动阻塞 try: # 提取文件名 filename = img_url.split('/')[-1] urllib.request.urlretrieve(img_url, f"{save_path}/{filename}") # 安全更新下载计数 with count_lock: download_count += 1 print(f"{download_count} {thread_name}") print(f"Done: {filename}") except Exception as e: print(f"{thread_name} 下载失败 {img_url}: {str(e)}") finally: task_queue.task_done() # 标记当前任务完成 def start_multi_thread_download(urls, thread_count=10): print('开始下载...') task_queue = Queue() # 把所有URL塞进任务队列 for url in urls: task_queue.put(url) # 创建并启动线程 threads = [] for i in range(thread_count): thread = Thread(target=download_worker, args=(task_queue, f"线程 {i+1}")) thread.daemon = True # 设置为守护线程,主程序结束时自动退出 thread.start() threads.append(thread) # 等待所有任务完成 task_queue.join() print("所有下载任务完成!") # 调用示例 if __name__ == "__main__": # 替换成你的21个URL集合 image_urls = {"url1", "url2", "url3", "..."} start_multi_thread_download(image_urls)
优化点说明:
queue.Queue管理任务:每个线程只从队列里拿未处理的任务,不会重复下载,也不用遍历整个集合,效率高多了,而且队列本身就是线程安全的,不用手动加锁判断。- 线程安全计数:用
Lock包裹计数更新,确保同一时间只有一个线程修改计数,不会出现计数错误。 - 异常捕获:单个URL下载失败不会导致整个线程崩溃,还能打印错误信息方便排查。
- 守护线程:主程序完成所有任务后,线程会自动退出,不用手动挨个join线程。
- 简洁的线程创建:用循环创建指定数量的线程,不用写10遍重复代码。
如果你非要用原来的集合方式(不推荐)
那除了把args=(queue)改成args=(queue,),还要给done集合和count加锁,避免线程安全问题:
import urllib.request from threading import Thread, Lock count = 0 count_lock = Lock() done = set() done_lock = Lock() path = 'foldername' def downloading(queue, name): global count for imgs in queue: with done_lock: if imgs in done: continue done.add(imgs) try: urllib.request.urlretrieve(imgs, path + '/' + imgs.split('/')[-1]) with count_lock: count += 1 print(str(count) + ' ' + name) print('Done: ' + imgs.split('/')[-1]) except Exception as e: print(f"{name} 下载失败 {imgs}: {str(e)}") def threads(queue): print('Start Downloading ...') thread_list = [] for i in range(10): th = Thread(target=downloading, args=(queue, f"Thread {i+1}")) th.start() thread_list.append(th) # 等待所有线程结束 for th in thread_list: th.join() print("All done!") # 调用 queue = {"some urls"} threads(queue)
但这种方式真的不如用队列的方案高效,还是推荐用第一个优化版本~
内容的提问来源于stack exchange,提问作者Albaraa
相关产品推荐
相关产品推荐

