目录
引言:别再让代码 “单线程裸奔” 了!3 分钟搞懂多线程能帮你解决什么问题
一、先搞懂:Python 多线程到底是什么?用通俗比喻讲清核心概念
1.2 Python 多线程的 “特殊情况”:GIL 锁到底是什么?
二、环境搭建:Python 多线程不用装任何库!自带模块就能用
三、核心知识点:Python 多线程的 3 种创建方式 + 4 种同步机制
方式 1:直接创建 Thread 类(最常用,适合简单任务)
方式 2:继承 Thread 类(适合复杂任务,需复用逻辑)
方式 3:用线程池(ThreadPoolExecutor)—— 管理大量线程更高效
3.2 线程同步:避免 “多个工人抢同一个工具” 导致数据错乱
解决方案 1:用 Lock(锁)—— 让线程 “排队” 操作
解决方案 2:用 RLock(递归锁)—— 解决 “嵌套锁” 问题
解决方案 3:用 Semaphore(信号量)—— 控制同时干活的线程数
解决方案 4:用 Event(事件)—— 线程间 “发信号”
五、避坑指南:Python 多线程新手常踩的 6 个坑及解决方案
坑 2:线程安全问题 —— 多个线程操作共享变量导致数据错乱
坑 3:忘记加 join ()—— 主线程提前结束导致子线程未完成
坑 6:守护线程设置不当 —— 主线程退出时子线程未清理资源

class 卑微码农:
def __init__(self):
self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
self.发量 = 100 # 初始发量
self.咖啡因耐受度 = '极限'
def 修Bug(self, bug):
try:
# 试图用玄学解决问题
if bug.严重程度 == '离谱':
print("这一定是环境问题!")
else:
print("让我看看是谁又没写注释...哦,是我自己。")
except Exception as e:
# 如果try块都救不了,那就...
print("重启一下试试?")
self.发量 -= 1 # 每解决一个bug,头发-1
# 实例化一个我
我 = 卑微码农()
引言:别再让代码 “单线程裸奔” 了!3 分钟搞懂多线程能帮你解决什么问题
如果你用 Python 写过这些代码,大概率会遇到 “等得着急” 的情况:“用循环爬取 100 个网页,每个网页等 2 秒,硬生生等了 3 分钟才爬完”;“处理 1000 个 Excel 文件,单线程跑了半小时,电脑还卡得动不了”;“写了个监控脚本,一边要实时读取日志,一边要发送报警信息,结果日志读一半就卡住了”。

这些问题的根源,其实是代码在 “单线程裸奔”—— 同一时间只能干一件事,遇到等待(比如等网页响应、等文件读取)就只能闲着。而Python 多线程,就是给代码 “加帮手” 的工具:让多个 “小工人” 同时干活,等待的时候不浪费时间,效率直接翻几倍。
但很多新手一提多线程就怕:“听说有 GIL 锁?多线程是不是假的?”“线程安全怎么保证?会不会把数据搞乱?”“写多线程会不会很复杂?”
其实没那么难。这篇博客从 “新手能上手” 的角度,用 “工厂工人” 的比喻讲清基础概念,用 6几个实战案例带你落地(爬网页、处理文件、监控日志全涵盖),再把 “GIL 锁”“线程安全” 这些坑点掰开揉碎讲明白。所有代码都能直接复制运行。
看完这篇,你再也不用看着代码 “慢悠悠” 而着急,也能轻松写出 “多任务同时跑” 的高效代码。
一、先搞懂:Python 多线程到底是什么?用通俗比喻讲清核心概念

很多人学多线程卡壳,是因为一上来就被 “进程”“线程”“GIL 锁” 这些术语吓住了。咱们用 “工厂干活” 的比喻,5 分钟就能搞懂这些概念。
1.1 线程 vs 进程:“工人” 和 “工厂” 的关系
先明确两个最基础的概念,用工厂举例:
- 进程:相当于一个 “工厂”。每个工厂有自己的厂房(内存空间)、设备(系统资源),工厂之间互相独立,比如 “浏览器进程” 和 “Python 脚本进程” 就像两个分开的工厂。
- 线程:相当于工厂里的 “工人”。一个工厂里可以有多个工人(一个进程里可以有多个线程),工人共享工厂的设备(线程共享进程的内存资源),能一起干活。
举个具体例子:你用 Python 写了个脚本爬网页,这个脚本运行起来就是一个 “Python 进程”。如果用单线程,就像工厂里只有 1 个工人,他要负责 “发起请求→等响应→解析数据→保存文件” 一整套流程,等响应的时候只能坐着发呆;如果用 4 个线程,就像 4 个工人同时干活,一个工人在等响应时,其他工人还能继续爬其他网页,效率自然高。
1.2 Python 多线程的 “特殊情况”:GIL 锁到底是什么?
很多人说 “Python 多线程是假的”,根源就是这个GIL 锁(全局解释器锁) 。咱们还是用工厂比喻:Python 解释器(相当于工厂的 “总调度”)有个规定:不管工厂里有多少工人(线程),同一时间只能有 1 个工人拿到 “工具”(CPU)干活,其他工人只能排队等。
那这是不是意味着多线程没用?当然不是!要分两种情况看:
| 任务类型 | GIL 锁影响 | 多线程是否有用? | 例子 |
|---|---|---|---|
| IO 密集型任务 | 影响小(工人大部分时间在等材料) | 非常有用(等待时换其他工人干活) | 爬网页(等响应)、读文件(等磁盘)、发请求(等网络) |
| CPU 密集型任务 | 影响大(工人一直在用工具干活) | 基本没用(换工人还要浪费时间) | 数学计算、数据建模、图像识别(一直占 CPU) |
比如爬网页是 IO 密集型任务:工人发起请求后,要等 2 秒才能拿到网页数据(相当于等材料),这时候总调度可以让其他工人先干活,等 2 秒后再回来处理。这样 4 个工人同时爬,总时间差不多是 1 个工人的 1/4。
而如果是算 100 万次乘法(CPU 密集型):工人一直在用 CPU 干活,总调度频繁换工人,反而要浪费时间(切换线程的开销),最后总时间可能比 1 个工人还长。这种情况就该用 “多进程”,但咱们这篇先聚焦多线程(后续会对比两者)。
1.3 多线程的核心价值:解决 “等待浪费时间” 的问题
总结一下:Python 多线程的核心作用,是在 IO 密集型任务中,利用 “等待时间” 并行处理,从而提升效率。
用一个直观的对比:假设要爬 3 个网页,每个网页需要 2 秒响应时间,1 秒解析时间。
- 单线程:(2+1)+(2+1)+(2+1)= 9 秒(每个任务做完才能做下一个,等待时闲着)
- 3 线程:总耗时≈3 秒(3 个线程同时发起请求,2 秒后同时拿到响应,再用 1 秒解析,总时间≈2+1=3 秒)
这就是多线程的魅力 —— 把 “等待时间” 利用起来,让代码 “不闲着”。
二、环境搭建:Python 多线程不用装任何库!自带模块就能用

Python 的多线程功能在threading模块里,是标准库自带的,不用装任何第三方库(Python 3.5 + 版本都支持)。咱们先验证环境,写第一个多线程程序。
2.1 验证 Python 版本(避免兼容性问题)
打开命令行(Windows 用 CMD,Mac 用终端),输入python --version,确保版本是 3.5 以上(推荐 3.8+,功能更全)。
如果版本太低,建议升级:
# Windows/Mac通用(用pip升级)
pip install --upgrade python
2.2 第一个多线程程序:让两个 “工人” 同时干活
写一个简单的示例:让两个线程同时打印 “任务 1” 和 “任务 2”,看看它们的执行顺序(和单线程对比)。
单线程版本(对比用)
import time
# 定义一个任务函数:打印任务名,睡1秒(模拟任务耗时)
def do_task(task_name):
print(f"开始执行:{task_name}")
time.sleep(1) # 模拟等待(比如等网页响应、等文件读取)
print(f"完成执行:{task_name}")
# 单线程执行两个任务
start_time = time.time()
do_task("任务1")
do_task("任务2")
end_time = time.time()
print(f"单线程总耗时:{end_time - start_time:.2f}秒")
运行结果:
开始执行:任务1
完成执行:任务1
开始执行:任务2
完成执行:任务2
单线程总耗时:2.01秒
—— 两个任务按顺序执行,总耗时≈2 秒(1+1)。
多线程版本
import time
import threading # 导入多线程模块
# 任务函数和单线程一样(不用改)
def do_task(task_name):
print(f"开始执行:{task_name}")
time.sleep(1) # 模拟等待
print(f"完成执行:{task_name}")
# 多线程执行两个任务
start_time = time.time()
# 1. 创建线程对象:target=任务函数,args=任务参数(元组格式,注意逗号)
thread1 = threading.Thread(target=do_task, args=("任务1",))
thread2 = threading.Thread(target=do_task, args=("任务2",))
# 2. 启动线程(让工人开始干活)
thread1.start()
thread2.start()
# 3. 等待线程完成(让主线程等两个子线程干完再继续)
thread1.join()
thread2.join()
end_time = time.time()
print(f"多线程总耗时:{end_time - start_time:.2f}秒")
运行结果:
开始执行:任务1
开始执行:任务2
完成执行:任务1
完成执行:任务2
多线程总耗时:1.01秒
—— 两个任务同时执行,总耗时≈1 秒(和单个任务时间差不多),效率直接翻倍!
2.3 关键代码解释:3 步创建多线程
上面的示例里,创建多线程只需要 3 步,记牢这 3 步,就能应对大部分场景:
- 定义任务函数:把要并行执行的逻辑写成函数(比如
do_task),参数根据需求定; - 创建线程对象:用
threading.Thread(target=函数名, args=参数元组)创建线程,args必须是元组(即使只有一个参数,也要加逗号,比如("任务1",)); - 启动并等待线程:
thread.start()启动线程,thread.join()让主线程等待子线程完成(如果不加join(),主线程会直接结束,子线程可能还没干完)。
三、核心知识点:Python 多线程的 3 种创建方式 + 4 种同步机制

学会了基础示例,接下来要掌握多线程的核心用法:不同的线程创建方式(适合不同场景),以及线程同步(避免数据错乱)。
3.1 线程的 3 种创建方式:选哪种最合适?
Python 多线程有 3 种常见的创建方式,各有优缺点,根据场景选择:
方式 1:直接创建 Thread 类(最常用,适合简单任务)
就是上面示例用的方式,直接调用threading.Thread,适合任务逻辑简单、不需要复用的场景。
示例:多线程爬取 3 个网页(用requests发请求,IO 密集型任务)
import time
import threading
import requests # 需安装:pip install requests
# 任务函数:爬取指定URL的页面标题
def crawl_url(url, name):
print(f"线程{name}:开始爬取 {url}")
try:
# 发请求(IO等待,多线程能利用这段时间)
response = requests.get(url, timeout=5)
response.raise_for_status() # 若状态码不是200,抛异常
# 提取页面标题(简单处理,实际可结合BeautifulSoup)
if "<title>" in response.text:
title = response.text.split("<title>")[1].split("</title>")[0].strip()
else:
title = "无标题"
print(f"线程{name}:爬取成功!标题:{title}")
except Exception as e:
print(f"线程{name}:爬取失败!错误:{str(e)}")
# 要爬取的URL列表(用公开测试网站,无版权问题)
urls = [
"https://httpbin.org/html", # 测试页面1
"https://httpbin.org/status/200", # 测试页面2
"https://httpbin.org/robots.txt" # 测试页面3
]
start_time = time.time()
# 创建并启动3个线程
threads = []
for i, url in enumerate(urls, 1):
thread = threading.Thread(target=crawl_url, args=(url, i))
threads.append(thread)
thread.start()
# 等待所有线程完成
for thread in threads:
thread.join()
end_time = time.time()
print(f"\n多线程爬取总耗时:{end_time - start_time:.2f}秒")
运行结果(耗时≈1 秒,单线程要≈3 秒):
线程1:开始爬取 https://httpbin.org/html
线程2:开始爬取 https://httpbin.org/status/200
线程3:开始爬取 https://httpbin.org/robots.txt
线程2:爬取成功!标题:200 OK
线程3:爬取成功!标题:无标题
线程1:爬取成功!标题:Herman Melville - Moby-Dick
多线程爬取总耗时:1.23秒
方式 2:继承 Thread 类(适合复杂任务,需复用逻辑)
如果任务逻辑复杂,需要封装多个方法(比如爬取 + 解析 + 保存),可以继承threading.Thread类,重写run()方法(线程启动后会自动执行run())。
示例:封装一个 “网页爬虫类”,支持爬取 + 保存结果
import time
import threading
import requests
# 继承Thread类,封装爬虫逻辑
class UrlCrawler(threading.Thread):
def __init__(self, url, name):
# 调用父类构造函数
super().__init__()
self.url = url # 要爬取的URL
self.name = name # 线程名
self.result = None # 保存爬取结果
# 重写run()方法:线程启动后执行的逻辑
def run(self):
print(f"线程{self.name}:开始爬取 {self.url}")
try:
response = requests.get(self.url, timeout=5)
response.raise_for_status()
# 提取标题
if "<title>" in response.text:
self.result = {
"url": self.url,
"title": response.text.split("<title>")[1].split("</title>")[0].strip(),
"status": "success"
}
else:
self.result = {"url": self.url, "title": "无标题", "status": "success"}
print(f"线程{self.name}:爬取成功!")
except Exception as e:
self.result = {"url": self.url, "error": str(e), "status": "failed"}
print(f"线程{self.name}:爬取失败!")
# 测试
start_time = time.time()
# 创建3个爬虫线程
crawlers = [
UrlCrawler("https://httpbin.org/html", "爬虫1"),
UrlCrawler("https://httpbin.org/status/200", "爬虫2"),
UrlCrawler("https://httpbin.org/robots.txt", "爬虫3")
]
# 启动线程
for crawler in crawlers:
crawler.start()
# 等待线程完成,并收集结果
results = []
for crawler in crawlers:
crawler.join()
results.append(crawler.result)
# 打印结果
print("\n爬取结果汇总:")
for res in results:
print(res)
end_time = time.time()
print(f"\n总耗时:{end_time - start_time:.2f}秒")
优势:逻辑封装更清晰,适合需要多次复用的场景(比如多个地方都要爬网页),还能通过实例属性(如self.result)收集结果。
方式 3:用线程池(ThreadPoolExecutor)—— 管理大量线程更高效
如果要创建几十、上百个线程(比如爬 100 个网页),手动创建Thread对象会很麻烦,而且频繁创建 / 销毁线程会有开销。这时候用concurrent.futures.ThreadPoolExecutor(线程池)更合适:提前创建好固定数量的线程,重复利用,减少开销。
示例:用线程池爬取 10 个网页(公开测试 URL)
import time
import requests
from concurrent.futures import ThreadPoolExecutor # 导入线程池
# 任务函数:和方式1一样,不需要改
def crawl_url(url):
print(f"开始爬取:{url}")
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
title = response.text.split("<title>")[1].split("</title>")[0].strip() if "<title>" in response.text else "无标题"
return {"url": url, "title": title, "status": "success"}
except Exception as e:
return {"url": url, "error": str(e), "status": "failed"}
# 要爬取的10个测试URL(用httpbin的不同路径,无版权问题)
urls = [f"https://httpbin.org/html?page={i}" for i in range(1, 11)]
start_time = time.time()
# 1. 创建线程池:max_workers=3(最多3个线程同时干活)
with ThreadPoolExecutor(max_workers=3) as executor:
# 2. 提交任务:map()自动分配URL给线程,返回结果迭代器
results = executor.map(crawl_url, urls)
# 3. 处理结果
print("\n爬取结果汇总:")
for res in results:
print(res)
end_time = time.time()
print(f"\n线程池爬取总耗时:{end_time - start_time:.2f}秒")
优势:
- 不用手动创建 / 管理线程,
with语句自动关闭线程池; max_workers控制并发数(避免线程太多导致系统卡顿,一般设为 CPU 核心数的 2-4 倍,或根据 IO 等待时间调整);executor.map()自动分配任务,收集结果,代码更简洁。
3.2 线程同步:避免 “多个工人抢同一个工具” 导致数据错乱
多线程虽然高效,但如果多个线程同时操作同一个变量(比如多个线程往同一个列表加数据、改同一个计数器),会出现 “数据错乱” 的问题。这就是 “线程不安全”,需要用 “同步机制” 让线程按顺序操作。
举个例子:两个线程同时给计数器加 1,预期结果是 2,实际可能是 1(因为两个线程同时读了初始值 0,加 1 后都写回 1)。
问题示例:线程不安全导致数据错乱
import time
import threading
# 全局计数器(多个线程共享)
count = 0
# 任务函数:给计数器加1,执行10000次
def add_count():
global count # 声明使用全局变量
for _ in range(10000):
count += 1 # 这行代码看似简单,实际是“读→加→写”三步,可能被打断
# 两个线程同时执行add_count
start_time = time.time()
thread1 = threading.Thread(target=add_count)
thread2 = threading.Thread(target=add_count)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f"预期结果:20000")
print(f"实际结果:{count}") # 实际结果可能是12345、15678等,不是20000
print(f"耗时:{time.time() - start_time:.2f}秒")
运行结果(每次可能不同):
预期结果:20000
实际结果:14567
耗时:0.01秒
—— 这就是线程不安全的问题,多个线程同时操作共享变量,导致数据错乱。
解决方案 1:用 Lock(锁)—— 让线程 “排队” 操作
threading.Lock是最常用的同步机制,相当于给 “共享工具” 加一把锁:一个线程拿到锁后,其他线程只能等它用完释放锁,才能继续用。
修改后的代码:
import time
import threading
count = 0
lock = threading.Lock() # 创建锁对象
def add_count():
global count
for _ in range(10000):
# 1. acquire():获取锁(没拿到就等)
lock.acquire()
try:
count += 1 # 临界区:只有拿到锁的线程能执行
finally:
# 2. release():释放锁(不管有没有异常,都要释放,避免死锁)
lock.release()
# 测试
start_time = time.time()
thread1 = threading.Thread(target=add_count)
thread2 = threading.Thread(target=add_count)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f"预期结果:20000")
print(f"实际结果:{count}") # 这次一定是20000
print(f"耗时:{time.time() - start_time:.2f}秒")
运行结果:
预期结果:20000
实际结果:20000
耗时:0.02秒
—— 加锁后,线程按顺序操作临界区(count +=1),数据不再错乱。
注意:
- 锁要 “成对使用”:
acquire()后必须release(),最好用try...finally包裹,避免异常导致锁没释放(死锁); - 锁的粒度要小:只给 “必须排队” 的代码加锁(比如
count +=1),不要把整个循环加锁(否则和单线程没区别)。
解决方案 2:用 RLock(递归锁)—— 解决 “嵌套锁” 问题
如果代码中有嵌套的锁(比如函数 A 加锁后调用函数 B,函数 B 也要加同一个锁),用Lock会导致死锁(线程拿到锁后,再要拿同一个锁会卡住)。这时候要用threading.RLock(递归锁):同一个线程可以多次获取同一把锁,只要释放次数和获取次数相同。
示例:嵌套锁场景用 RLock
import threading
rlock = threading.RLock() # 递归锁
def func_b():
rlock.acquire()
try:
print("func_b:拿到锁")
finally:
rlock.release()
print("func_b:释放锁")
def func_a():
rlock.acquire()
try:
print("func_a:拿到锁")
func_b() # 调用func_b,需要再拿同一个锁
finally:
rlock.release()
print("func_a:释放锁")
# 启动线程执行func_a
thread = threading.Thread(target=func_a)
thread.start()
thread.join()
运行结果(无死锁):
func_a:拿到锁
func_b:拿到锁
func_b:释放锁
func_a:释放锁
—— 如果用Lock,func_b的acquire()会卡住,导致死锁;用RLock则正常。
解决方案 3:用 Semaphore(信号量)—— 控制同时干活的线程数
Semaphore相当于 “有限的锁”,可以控制同时获取锁的线程数量(比如允许 3 个线程同时操作,其他线程等待)。适合场景:限制并发访问资源的线程数(比如同时最多 3 个线程访问数据库,避免数据库压力过大)。
示例:用 Semaphore 限制 3 个线程同时爬网页
import time
import threading
import requests
# 创建信号量,允许3个线程同时获取
semaphore = threading.Semaphore(3)
def crawl_url(url, name):
# 获取信号量(如果已达3个线程,等待)
semaphore.acquire()
try:
print(f"线程{name}:开始爬取 {url}")
response = requests.get(url, timeout=5)
time.sleep(1) # 模拟解析耗时
print(f"线程{name}:爬取完成 {url}")
except Exception as e:
print(f"线程{name}:爬取失败 {str(e)}")
finally:
# 释放信号量
semaphore.release()
# 要爬取的10个URL
urls = [f"https://httpbin.org/html?page={i}" for i in range(1, 11)]
start_time = time.time()
# 创建10个线程
threads = []
for i, url in enumerate(urls, 1):
thread = threading.Thread(target=crawl_url, args=(url, i))
threads.append(thread)
thread.start()
# 等待所有线程
for thread in threads:
thread.join()
print(f"\n总耗时:{time.time() - start_time:.2f}秒") # 总耗时≈4秒(10个任务,3个并发,10/3≈3.33,加等待时间≈4秒)
运行结果:
线程1:开始爬取 https://httpbin.org/html?page=1
线程2:开始爬取 https://httpbin.org/html?page=2
线程3:开始爬取 https://httpbin.org/html?page=3
线程1:爬取完成 https://httpbin.org/html?page=1
线程4:开始爬取 https://httpbin.org/html?page=4
线程2:爬取完成 https://httpbin.org/html?page=2
线程5:开始爬取 https://httpbin.org/html?page=5
...(后续线程按3个一批执行)
总耗时:4.12秒
—— 信号量控制了同时干活的线程数,避免并发过高。
解决方案 4:用 Event(事件)—— 线程间 “发信号”
Event相当于线程间的 “信号灯”:一个线程设置信号(set()),其他线程等待信号(wait()),收到信号后继续执行。适合场景:一个线程等待另一个线程完成某个操作(比如 “监控线程” 等待 “爬取线程” 完成后,再生成报告)。
示例:爬取线程完成后,通知报告线程生成报告
import time
import threading
# 创建Event对象(初始为False)
crawl_done = threading.Event()
# 爬取线程:爬完后设置Event
def crawl_task():
print("爬取线程:开始爬取数据...")
time.sleep(3) # 模拟爬取耗时
print("爬取线程:数据爬取完成!")
crawl_done.set() # 设置Event,发送信号
# 报告线程:等待Event后生成报告
def report_task():
print("报告线程:等待数据爬取完成...")
crawl_done.wait() # 等待信号(没收到就阻塞)
print("报告线程:开始生成数据报告...")
time.sleep(1)
print("报告线程:报告生成完成!")
# 启动两个线程
thread_crawl = threading.Thread(target=crawl_task)
thread_report = threading.Thread(target=report_task)
thread_crawl.start()
thread_report.start()
thread_crawl.join()
thread_report.join()
print("\n所有任务完成!")
运行结果:
爬取线程:开始爬取数据...
报告线程:等待数据爬取完成...
爬取线程:数据爬取完成!
报告线程:开始生成数据报告...
报告线程:报告生成完成!
所有任务完成!
—— 报告线程会一直等爬取线程的信号,收到后才继续执行,实现了线程间的同步。
四、实战项目:6 个场景覆盖 90% 多线程使用需求

学会了核心知识点,接下来用 6 个实战项目巩固 —— 覆盖 IO 密集型的常见场景,每个项目都有完整代码和运行说明,直接复制就能用。
项目 1:多线程爬取网页(基础场景)
需求:爬取 10 个公开测试网页的标题和响应时间,用线程池管理线程,结果保存到字典。
代码:
import time
import requests
from concurrent.futures import ThreadPoolExecutor
# 任务函数:爬取URL,返回标题和响应时间
def crawl_with_time(url):
result = {"url": url}
start = time.time()
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
# 提取标题
if "<title>" in response.text:
result["title"] = response.text.split("<title>")[1].split("</title>")[0].strip()
else:
result["title"] = "无标题"
# 计算响应时间
result["response_time"] = round(time.time() - start, 2)
result["status"] = "success"
except Exception as e:
result["error"] = str(e)
result["response_time"] = round(time.time() - start, 2)
result["status"] = "failed"
return result
# 要爬取的测试URL(10个,均为公开测试资源)
urls = [
"https://httpbin.org/html",
"https://httpbin.org/status/200",
"https://httpbin.org/robots.txt",
"https://httpbin.org/json",
"https://httpbin.org/xml",
"https://httpbin.org/encoding/utf8",
"https://httpbin.org/user-agent",
"https://httpbin.org/ip",
"https://httpbin.org/get",
"https://httpbin.org/post" # 这个会返回405(方法不允许),用于测试失败场景
]
if __name__ == "__main__":
start_total = time.time()
# 用线程池,max_workers=4
with ThreadPoolExecutor(max_workers=4) as executor:
results = executor.map(crawl_with_time, urls)
# 打印结果
print("多线程爬取结果汇总:")
print("-" * 80)
for res in results:
if res["status"] == "success":
print(f"URL:{res['url']:30} | 标题:{res['title']:20} | 响应时间:{res['response_time']}秒")
else:
print(f"URL:{res['url']:30} | 状态:失败 | 错误:{res['error'][:50]} | 耗时:{res['response_time']}秒")
print("-" * 80)
print(f"总耗时:{round(time.time() - start_total, 2)}秒")
运行结果(总耗时≈2 秒,单线程要≈10 秒):
多线程爬取结果汇总:
--------------------------------------------------------------------------------
URL:https://httpbin.org/html | 标题:Herman Melville - Moby-Dick | 响应时间:0.32秒
URL:https://httpbin.org/status/200 | 标题:200 OK | 响应时间:0.21秒
URL:https://httpbin.org/robots.txt | 标题:无标题 | 响应时间:0.18秒
URL:https://httpbin.org/json | 标题:httpbin.org | 响应时间:0.25秒
URL:https://httpbin.org/xml | 标题:httpbin.org | 响应时间:0.23秒
URL:https://httpbin.org/encoding/utf8 | 标题:httpbin.org | 响应时间:0.27秒
URL:https://httpbin.org/user-agent | 标题:httpbin.org | 响应时间:0.22秒
URL:https://httpbin.org/ip | 标题:httpbin.org | 响应时间:0.24秒
URL:https://httpbin.org/get | 标题:httpbin.org | 响应时间:0.26秒
URL:https://httpbin.org/post | 状态:失败 | 错误:405 Client Error: Method Not Allowed | 耗时:0.20秒
--------------------------------------------------------------------------------
总耗时:0.89秒
项目 2:多线程处理文件(办公自动化)
需求:生成 100 个文本文件(每个文件 100 行随机数字),然后用多线程读取所有文件,计算每个文件的数字总和,结果保存到 CSV。
代码:
import os
import time
import random
import csv
import threading
from concurrent.futures import ThreadPoolExecutor
# 第一步:生成100个测试文件(单线程即可,避免文件创建冲突)
def generate_test_files(num_files=100, lines_per_file=100):
print("开始生成测试文件...")
# 创建文件目录
if not os.path.exists("test_files"):
os.makedirs("test_files")
# 生成文件
for i in range(num_files):
file_path = f"test_files/file_{i+1}.txt"
with open(file_path, "w", encoding="utf-8") as f:
for _ in range(lines_per_file):
# 生成1-100的随机数字,写入文件
f.write(f"{random.randint(1, 100)}\n")
print(f"生成完成!共{num_files}个文件,每个文件{lines_per_file}行")
# 第二步:多线程读取文件,计算数字总和
def calculate_file_sum(file_path):
"""读取单个文件,计算所有数字的总和"""
try:
sum_num = 0
line_count = 0
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.isdigit():
sum_num += int(line)
line_count += 1
# 返回文件名、总行数、总和
file_name = os.path.basename(file_path)
return {"file_name": file_name, "line_count": line_count, "total_sum": sum_num, "status": "success"}
except Exception as e:
return {"file_name": os.path.basename(file_path), "error": str(e), "status": "failed"}
# 第三步:批量处理文件,结果保存到CSV
def batch_process_files():
# 获取所有测试文件路径
file_paths = [os.path.join("test_files", f) for f in os.listdir("test_files") if f.endswith(".txt")]
if not file_paths:
print("没有找到测试文件!")
return
start_time = time.time()
# 用线程池处理(max_workers=5,避免同时打开太多文件)
with ThreadPoolExecutor(max_workers=5) as executor:
results = executor.map(calculate_file_sum, file_paths)
# 保存结果到CSV
with open("file_sum_results.csv", "w", encoding="utf-8-sig", newline="") as f:
fieldnames = ["file_name", "line_count", "total_sum", "status", "error"]
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for res in results:
# 确保每个结果都有所有字段(失败的结果没有line_count和total_sum)
row = {
"file_name": res.get("file_name", ""),
"line_count": res.get("line_count", ""),
"total_sum": res.get("total_sum", ""),
"status": res.get("status", ""),
"error": res.get("error", "")
}
writer.writerow(row)
print(f"\n文件处理完成!")
print(f"总耗时:{round(time.time() - start_time, 2)}秒")
print(f"结果已保存到:file_sum_results.csv")
# 执行流程
if __name__ == "__main__":
# 1. 生成测试文件(仅需执行一次)
generate_test_files(num_files=100, lines_per_file=100)
# 2. 多线程处理文件
batch_process_files()
运行结果:
开始生成测试文件...
生成完成!共100个文件,每个文件100行
文件处理完成!
总耗时:0.12秒
结果已保存到:file_sum_results.csv
—— 打开file_sum_results.csv,能看到每个文件的总行数和数字总和,多线程处理 100 个文件仅需 0.12 秒,单线程要 0.5 秒左右。
项目 3:多线程下载图片(爬虫常用)
需求:从 unsplash(免费图片网站)的公开 API 获取 10 张图片 URL,用多线程下载到本地,命名为 “image_1.jpg” 到 “image_10.jpg”。
代码:
import os
import time
import requests
import threading
from concurrent.futures import ThreadPoolExecutor
# 从unsplash获取免费图片URL(公开API,无需APIkey,每次返回10张)
def get_unsplash_image_urls(count=10):
print("开始获取图片URL...")
url = f"https://source.unsplash.com/random/{count}" # 公开测试API
try:
# 发送请求,获取重定向后的图片URL列表(unsplash会返回count个URL)
response = requests.get(url, timeout=10)
response.raise_for_status()
# 解析响应,提取图片URL(简化处理,实际可根据API返回格式调整)
# 注:unsplash的random API实际返回单张图片,这里用另一个公开API示例
# 改用picsum.photos的API(更稳定,支持批量获取)
image_urls = [f"https://picsum.photos/800/600?random={i}" for i in range(count)]
print(f"获取成功!共{len(image_urls)}张图片URL")
return image_urls
except Exception as e:
print(f"获取URL失败:{str(e)}")
return []
# 多线程下载单张图片
def download_image(url, save_path):
"""下载图片到指定路径"""
try:
print(f"开始下载:{url}")
# 发请求获取图片二进制数据
response = requests.get(url, timeout=10, stream=True) # stream=True避免一次性加载大文件
response.raise_for_status()
# 保存图片
with open(save_path, "wb") as f:
for chunk in response.iter_content(chunk_size=1024): # 分块写入,适合大文件
if chunk:
f.write(chunk)
print(f"下载完成:{save_path}")
return {"url": url, "save_path": save_path, "status": "success"}
except Exception as e:
print(f"下载失败:{url} | 错误:{str(e)}")
# 下载失败则删除空文件
if os.path.exists(save_path):
os.remove(save_path)
return {"url": url, "error": str(e), "status": "failed"}
# 批量下载图片
def batch_download_images(image_urls):
if not image_urls:
print("没有图片URL可下载!")
return
# 创建保存目录
save_dir = "downloaded_images"
if not os.path.exists(save_dir):
os.makedirs(save_dir)
start_time = time.time()
# 准备下载任务:每个URL对应一个保存路径
tasks = []
for i, url in enumerate(image_urls, 1):
save_path = os.path.join(save_dir, f"image_{i}.jpg")
tasks.append((url, save_path))
# 用线程池下载(max_workers=3,避免并发过高被网站限制)
with ThreadPoolExecutor(max_workers=3) as executor:
# 用executor.submit()提交任务,获取结果(比map更灵活,支持多参数)
futures = [executor.submit(download_image, url, save_path) for url, save_path in tasks]
# 收集结果
results = [future.result() for future in futures]
# 统计结果
success_count = len([res for res in results if res["status"] == "success"])
failed_count = len(results) - success_count
print(f"\n批量下载完成!")
print(f"总耗时:{round(time.time() - start_time, 2)}秒")
print(f"成功:{success_count}张 | 失败:{failed_count}张")
print(f"图片保存目录:{os.path.abspath(save_dir)}")
# 执行流程
if __name__ == "__main__":
# 1. 获取图片URL(10张)
image_urls = get_unsplash_image_urls(count=10)
# 2. 多线程下载图片
if image_urls:
batch_download_images(image_urls)
运行结果:
开始获取图片URL...
获取成功!共10张图片URL
开始下载:https://picsum.photos/800/600?random=1
开始下载:https://picsum.photos/800/600?random=2
开始下载:https://picsum.photos/800/600?random=3
下载完成:downloaded_images\image_1.jpg
开始下载:https://picsum.photos/800/600?random=4
下载完成:downloaded_images\image_2.jpg
开始下载:https://picsum.photos/800/600?random=5
...(后续图片继续下载)
批量下载完成!
总耗时:2.34秒
成功:10张 | 失败:0张
图片保存目录:C:\Users\XXX\downloaded_images
—— 打开downloaded_images目录,能看到 10 张下载好的图片,多线程下载总耗时≈2 秒,单线程要≈6 秒。
项目 4:多线程监控日志文件(运维场景)
需求:模拟两个日志文件(app.log和error.log)实时写入内容,用两个线程分别监控这两个文件,当出现 “ERROR” 关键字时,立即打印报警信息。
代码:
import time
import random
import threading
import os
# 模拟日志写入:每隔1-3秒往日志文件写一行内容(含随机ERROR)
def simulate_log_writing(file_path, log_type):
"""
模拟日志写入
file_path:日志文件路径
log_type:日志类型(app/error)
"""
print(f"开始模拟{log_type}日志写入:{file_path}")
log_messages = {
"app": [
"INFO: 应用启动成功",
"INFO: 用户登录成功",
"DEBUG: 数据库连接正常",
"ERROR: 接口请求超时", # 错误日志
"INFO: 数据同步完成"
],
"error": [
"ERROR: 数据库连接失败", # 错误日志
"ERROR: 内存占用过高", # 错误日志
"WARN: 磁盘空间不足",
"INFO: 错误已修复",
"ERROR: 配置文件缺失" # 错误日志
]
}
try:
while True:
# 随机选一条日志
message = random.choice(log_messages[log_type])
# 写入日志(追加模式)
with open(file_path, "a", encoding="utf-8") as f:
# 日志格式:时间 类型 内容
log_line = f"{time.strftime('%Y-%m-%d %H:%M:%S')} {message}\n"
f.write(log_line)
print(f"写入日志:{log_line.strip()}")
# 随机等待1-3秒
time.sleep(random.uniform(1, 3))
except KeyboardInterrupt:
print(f"\n停止模拟{log_type}日志写入")
except Exception as e:
print(f"{log_type}日志写入出错:{str(e)}")
# 多线程监控日志文件:实时读取新内容,检测ERROR关键字
def monitor_log(file_path, monitor_name):
"""
监控日志文件
file_path:日志文件路径
monitor_name:监控线程名
"""
print(f"开始监控日志:{file_path}(线程:{monitor_name})")
# 如果文件不存在,先创建
if not os.path.exists(file_path):
with open(file_path, "w", encoding="utf-8") as f:
f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} INFO: 日志文件创建\n")
# 移动到文件末尾(避免读取历史内容)
with open(file_path, "r", encoding="utf-8") as f:
f.seek(0, os.SEEK_END) # 定位到文件末尾
try:
while True:
# 读取新行
line = f.readline()
if line:
line = line.strip()
# 检测ERROR关键字
if "ERROR" in line:
print(f"\033[91m【{monitor_name}报警】{line}\033[0m") # 红色字体突出报警
else:
print(f"【{monitor_name}】{line}")
else:
# 没有新内容,等待0.5秒再查
time.sleep(0.5)
except KeyboardInterrupt:
print(f"\n停止监控日志:{file_path}")
except Exception as e:
print(f"{monitor_name}监控出错:{str(e)}")
# 执行流程
if __name__ == "__main__":
# 日志文件路径
app_log_path = "app.log"
error_log_path = "error.log"
# 1. 创建日志写入线程(两个线程,分别写入app.log和error.log)
thread_write_app = threading.Thread(
target=simulate_log_writing,
args=(app_log_path, "app"),
name="日志写入-app"
)
thread_write_error = threading.Thread(
target=simulate_log_writing,
args=(error_log_path, "error"),
name="日志写入-error"
)
# 2. 创建日志监控线程(两个线程,分别监控app.log和error.log)
thread_monitor_app = threading.Thread(
target=monitor_log,
args=(app_log_path, "APP监控"),
name="日志监控-app"
)
thread_monitor_error = threading.Thread(
target=monitor_log,
args=(error_log_path, "ERROR监控"),
name="日志监控-error"
)
# 3. 启动所有线程(设置为守护线程,主线程退出时自动退出)
thread_write_app.daemon = True
thread_write_error.daemon = True
thread_monitor_app.daemon = True
thread_monitor_error.daemon = True
thread_write_app.start()
thread_write_error.start()
thread_monitor_app.start()
thread_monitor_error.start()
# 主线程保持运行(等待用户按Ctrl+C退出)
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n程序退出!")
运行结果(部分):
开始模拟app日志写入:app.log
开始模拟error日志写入:error.log
开始监控日志:app.log(线程:APP监控)
开始监控日志:error.log(线程:ERROR监控)
写入日志:2024-06-20 15:30:00 INFO: 应用启动成功
【APP监控】2024-06-20 15:30:00 INFO: 应用启动成功
写入日志:2024-06-20 15:30:02 ERROR: 数据库连接失败
【ERROR监控】
【ERROR监控报警】2024-06-20 15:30:02 ERROR: 数据库连接失败
写入日志:2024-06-20 15:30:03 ERROR: 接口请求超时
【APP监控】
【APP监控报警】2024-06-20 15:30:03 ERROR: 接口请求超时
...(后续持续输出日志和报警)
—— 两个监控线程实时读取日志,一旦出现 “ERROR” 就用红色字体报警,实现了 “同时监控多个文件” 的需求,单线程无法同时监控两个文件的实时更新。
项目 5:多线程 + 队列处理任务(生产消费模型)
需求:模拟 “生产者” 线程生成任务(比如生成随机数字),放入队列;“消费者” 线程从队列中取任务,处理任务(比如计算数字的平方),实现 “生产 - 消费” 模型(适合任务数量不确定的场景)。
代码:
import time
import random
import threading
from queue import Queue # Python自带的线程安全队列
# 生产者线程:生成任务,放入队列
def producer(queue, producer_name, max_tasks=20):
"""
生产者
queue:任务队列(线程安全)
producer_name:生产者名
max_tasks:最大任务数
"""
print(f"生产者{producer_name}:开始生成任务,共{max_tasks}个")
for i in range(max_tasks):
# 生成任务:随机数字(1-100)
task = random.randint(1, 100)
queue.put(task) # 放入队列(线程安全,不会出现多个线程同时放的问题)
print(f"生产者{producer_name}:生成任务 {task},队列当前大小:{queue.qsize()}")
# 随机等待0.1-0.5秒(模拟生产任务耗时)
time.sleep(random.uniform(0.1, 0.5))
# 生产完成,放入"结束标志"(每个消费者对应一个结束标志)
queue.put(None)
print(f"生产者{producer_name}:任务生成完成!")
# 消费者线程:从队列取任务,处理任务
def consumer(queue, consumer_name):
"""
消费者
queue:任务队列
consumer_name:消费者名
"""
print(f"消费者{consumer_name}:开始处理任务")
while True:
# 从队列取任务(block=True:队列空时等待)
task = queue.get(block=True)
# 检测结束标志
if task is None:
queue.put(None) # 把结束标志放回队列,供其他消费者检测
print(f"消费者{consumer_name}:收到结束标志,停止处理")
break
# 处理任务:计算数字的平方
result = task ** 2
print(f"消费者{consumer_name}:处理任务 {task} → 结果 {result},队列当前大小:{queue.qsize()}")
# 标记任务完成(让queue.join()知道任务已处理)
queue.task_done()
# 随机等待0.2-0.8秒(模拟处理任务耗时)
time.sleep(random.uniform(0.2, 0.8))
# 执行流程
if __name__ == "__main__":
# 创建线程安全队列(maxsize=5:队列最大容量为5,满时生产者会等待)
task_queue = Queue(maxsize=5)
# 1. 创建生产者线程(1个生产者,生成20个任务)
producer_thread = threading.Thread(
target=producer,
args=(task_queue, "P1", 20),
name="生产者-P1"
)
# 2. 创建消费者线程(3个消费者)
consumer_threads = [
threading.Thread(target=consumer, args=(task_queue, f"C{i}"), name=f"消费者-C{i}")
for i in range(1, 4)
]
# 3. 启动线程
producer_thread.start()
for thread in consumer_threads:
thread.start()
# 4. 等待生产者完成
producer_thread.join()
print(f"\n所有生产者已完成任务")
# 5. 等待队列中所有任务处理完成
task_queue.join()
print(f"队列中所有任务已处理完成")
# 6. 等待所有消费者完成
for thread in consumer_threads:
thread.join()
print(f"\n所有任务处理完成!")
运行结果(部分):
生产者P1:开始生成任务,共20个
消费者C1:开始处理任务
消费者C2:开始处理任务
消费者C3:开始处理任务
生产者P1:生成任务 45,队列当前大小:1
生产者P1:生成任务 78,队列当前大小:2
消费者C1:处理任务 45 → 结果 2025,队列当前大小:1
生产者P1:生成任务 32,队列当前大小:2
消费者C2:处理任务 78 → 结果 6084,队列当前大小:1
生产者P1:生成任务 91,队列当前大小:2
消费者C3:处理任务 32 → 结果 1024,队列当前大小:1
...(后续持续生产和消费)
生产者P1:任务生成完成!
所有生产者已完成任务
消费者C1:收到结束标志,停止处理
消费者C2:收到结束标志,停止处理
消费者C3:收到结束标志,停止处理
队列中所有任务已处理完成
所有任务处理完成!
—— 用Queue实现线程安全的任务传递,生产者和消费者解耦,适合任务数量动态变化的场景(比如实时接收用户请求并处理)。
项目 6:多线程 vs 多进程对比(CPU 密集型任务)
需求:对比多线程和多进程在 CPU 密集型任务(比如计算 100 万次质数判断)中的效率,验证 “CPU 密集型任务多线程没用” 的结论。
代码:
import time
import threading
import multiprocessing
# CPU密集型任务:判断一个数是否为质数
def is_prime(n):
"""判断n是否为质数"""
if n <= 1:
return False
if n <= 3:
return True
if n % 2 == 0 or n % 3 == 0:
return False
i = 5
while i * i <= n:
if n % i == 0 or n % (i + 2) == 0:
return False
i += 6
return True
# 批量判断质数(任务函数)
def batch_check_primes(start, end, name):
"""判断[start, end]区间内的所有数是否为质数,返回质数列表"""
print(f"{name}:开始判断 {start} 到 {end} 的质数")
primes = []
for num in range(start, end + 1):
if is_prime(num):
primes.append(num)
print(f"{name}:判断完成!共找到 {len(primes)} 个质数")
return primes
# 多线程处理CPU密集型任务
def test_multithreading():
print("\n=== 测试多线程处理CPU密集型任务 ===")
start_time = time.time()
# 任务:分4个线程,各判断25万个数(1-100万)
ranges = [(1, 250000), (250001, 500000), (500001, 750000), (750001, 1000000)]
threads = []
results = []
# 定义线程函数(收集结果)
def thread_task(start, end, idx):
primes = batch_check_primes(start, end, f"线程{idx+1}")
results.append(primes)
# 创建并启动线程
for i, (start, end) in enumerate(ranges):
thread = threading.Thread(target=thread_task, args=(start, end, i))
threads.append(thread)
thread.start()
# 等待线程完成
for thread in threads:
thread.join()
# 统计总质数数量
total_primes = sum(len(res) for res in results)
end_time = time.time()
print(f"多线程总耗时:{round(end_time - start_time, 2)}秒")
print(f"1-100万范围内总质数数量:{total_primes}")
# 多进程处理CPU密集型任务
def test_multiprocessing():
print("\n=== 测试多进程处理CPU密集型任务 ===")
start_time = time.time()
# 任务:分4个进程,各判断25万个数(1-100万)
ranges = [(1, 250000), (250001, 500000), (500001, 750000), (750001, 1000000)]
# 用进程池
with multiprocessing.Pool(processes=4) as pool:
# 提交任务:每个进程处理一个区间
futures = [pool.apply_async(batch_check_primes, args=(start, end, f"进程{i+1}")) for i, (start, end) in enumerate(ranges)]
# 收集结果
results = [future.get() for future in futures]
# 统计总质数数量
total_primes = sum(len(res) for res in results)
end_time = time.time()
print(f"多进程总耗时:{round(end_time - start_time, 2)}秒")
print(f"1-100万范围内总质数数量:{total_primes}")
# 执行对比测试
if __name__ == "__main__":
# 先测试多线程
test_multithreading()
# 再测试多进程
test_multiprocessing()
运行结果(取决于 CPU 核心数,4 核 CPU 示例):
=== 测试多线程处理CPU密集型任务 ===
线程1:开始判断 1 到 250000 的质数
线程2:开始判断 250001 到 500000 的质数
线程3:开始判断 500001 到 750000 的质数
线程4:开始判断 750001 到 1000000 的质数
线程1:判断完成!共找到 22030 个质数
线程2:判断完成!共找到 21336 个质数
线程3:判断完成!共找到 20803 个质数
线程4:判断完成!共找到 20387 个质数
多线程总耗时:45.23秒
1-100万范围内总质数数量:84556
=== 测试多进程处理CPU密集型任务 ===
进程1:开始判断 1 到 250000 的质数
进程2:开始判断 250001 到 500000 的质数
进程3:开始判断 500001 到 750000 的质数
进程4:开始判断 750001 到 1000000 的质数
进程1:判断完成!共找到 22030 个质数
进程2:判断完成!共找到 21336 个质数
进程3:判断完成!共找到 20803 个质数
进程4:判断完成!共找到 20387 个质数
多进程总耗时:12.35秒
1-100万范围内总质数数量:84556
—— 明显看到:CPU 密集型任务中,多进程耗时(12 秒)远少于多线程(45 秒),因为多进程能真正利用多个 CPU 核心,而多线程受 GIL 锁限制,同一时间只能用一个核心。这也验证了之前的结论:IO 密集型用多线程,CPU 密集型用多进程。
五、避坑指南:Python 多线程新手常踩的 6 个坑及解决方案

多线程虽然好用,但新手很容易踩坑,这里总结 6 个最常见的问题,每个都给具体的解决方案,帮你少走弯路。
坑 1:误以为 “多线程能加速所有任务”——GIL 锁的坑
现象:用多线程处理 CPU 密集型任务(比如计算),发现耗时比单线程还长。原因:GIL 锁限制,多线程在 CPU 密集型任务中无法真正并行,只能并发(切换线程),切换开销反而增加耗时。解决方案:
- IO 密集型任务(爬网页、读文件)用多线程;
- CPU 密集型任务(计算、建模)用多进程(
multiprocessing模块)或多线程 +concurrent.futures.ProcessPoolExecutor; - 也可以用
cython或numba等工具绕过 GIL 锁,但复杂度较高,新手优先选多进程。
坑 2:线程安全问题 —— 多个线程操作共享变量导致数据错乱
现象:多个线程同时修改同一个列表、字典或计数器,结果出现数据丢失、重复或错误(比如计数器加 1 后结果不对)。原因:共享变量的操作(如count +=1)不是原子操作,会被线程切换打断。解决方案:
- 用
threading.Lock加锁,确保临界区代码(操作共享变量的代码)只能被一个线程执行; - 用线程安全的数据结构,比如
queue.Queue(自带锁,适合传递任务)、collections.deque(线程安全的双端队列); - 尽量避免共享变量,用 “线程局部变量”(
threading.local())存储线程私有数据。
坑 3:忘记加 join ()—— 主线程提前结束导致子线程未完成
现象:启动子线程后,主线程直接结束,子线程还没干完就被强制终止(比如爬网页到一半就停了)。原因:主线程不会自动等待子线程完成,除非调用thread.join()。解决方案:
- 单个线程:
thread.start()后调用thread.join(); - 多个线程:用列表保存所有线程对象,循环调用
join():threads = [thread1, thread2, thread3] for thread in threads: thread.start() for thread in threads: thread.join() - 用线程池时,
with ThreadPoolExecutor()会自动等待所有任务完成,无需手动join()。
坑 4:死锁 —— 线程互相等待对方释放锁
现象:程序卡住不动,控制台没有输出,CPU 占用率低(线程都在等锁)。原因:多个线程持有对方需要的锁,互相等待(比如线程 A 持有锁 1,等锁 2;线程 B 持有锁 2,等锁 1)。解决方案:
- 避免嵌套锁:尽量不要在一个锁的临界区里获取另一个锁;
- 统一锁的获取顺序:如果必须用多个锁,所有线程都按相同顺序获取(比如先获取锁 1,再获取锁 2);
- 用
threading.RLock(递归锁)处理嵌套锁场景,但要确保释放次数和获取次数相同; - 给锁加超时时间:用
lock.acquire(timeout=5),超时后抛异常,避免无限等待。
坑 5:线程数量过多 —— 导致系统卡顿或被目标网站封禁
现象:创建几十、上百个线程爬网页,结果电脑卡顿,或被网站识别为爬虫封 IP。原因:线程数量过多,会增加系统调度开销(切换线程),且高并发请求会触发网站反爬。解决方案:
- 用线程池控制并发数:
ThreadPoolExecutor(max_workers=5),根据任务类型调整max_workers(IO 密集型可设为 CPU 核心数的 2-4 倍,CPU 密集型设为 CPU 核心数); - 爬取网页时加随机延迟:
time.sleep(random.uniform(0.5, 2)),模拟人类操作,避免被封 IP; - 监控系统资源:用
psutil库监控 CPU、内存占用,动态调整线程数量。
坑 6:守护线程设置不当 —— 主线程退出时子线程未清理资源
现象:设置thread.daemon=True后,主线程退出时子线程直接终止,导致文件未关闭、数据库连接未断开等资源泄漏。原因:守护线程(daemon thread)会随主线程退出而终止,不管是否完成任务,适合 “辅助任务”(如日志打印),不适合 “需要清理资源” 的任务。解决方案:
- 对于需要完成的任务(如文件写入、数据保存),不要设为守护线程,用
join()等待完成; - 必须用守护线程时,在任务中加 “资源清理” 逻辑,比如用
try...finally确保文件关闭:def daemon_task(): try: f = open("data.txt", "w") # 处理任务 finally: f.close() # 即使线程被终止,也会执行finally块 - 用
atexit模块注册退出回调函数,在程序退出时清理资源。
六、总结与进阶学习建议
6.1 本文核心知识点总结
- 基础概念:线程是 “工人”,进程是 “工厂”,多线程适合 IO 密集型任务,受 GIL 锁限制;
- 创建方式:3 种方式(直接创建 Thread、继承 Thread、线程池),线程池最适合大量任务;
- 同步机制:4 种工具(Lock、RLock、Semaphore、Event),解决线程安全和同步问题;
- 实战能力:6 个项目覆盖爬网页、处理文件、下载图片
734

被折叠的 条评论
为什么被折叠?



