1. 预备知识:先搞懂这些“行话”
1.1 进程 vs 线程
进程(Process):进程是操作系统资源分配的基本单位,拥有独立的内存空间。
线程(Thread):CPU调度的基本单位,依附于进程、共享进程的内存空间。
举个栗子🌰:
比如我们运行 QQ 时就是一个进程,而 QQ 内部又同时运行着聊天线程、视频线程、图片接收线程……等等各种微小的功能线程,线程是基于进程的,线程可能同时有很多个。
| 特性 | 进程(Process) | 线程(Thread) |
| 内存 | 独立内存空间,安全性高 | 共享内存空间,需处理竞争条件 |
| 创建/销毁 | 大(需分配资源) | 小(共享资源) |
| 通信方式 | 管道、Socket等进程间通信(IPC) | 直接读写共享变量 |
| Python模块 | multiprocessing | threading |
1.2 并发 vs 并行
并发(Concurrency):通过快速切换任务,实现“看似同时”执行,单核CPU也能做到。
并行(Parallelism):真正的同时执行,依赖多核CPU。
举个栗子🌰:
假如我的电脑是单核的,我可以一边听歌,一边上网、一边打游戏、一边微信聊天,而且看起来是同时在运行!但实际上它们是快速交替在 CPU 上运行,比如音乐软件运行 20 毫秒后游戏再运行 20 毫秒,由于时间片微小,我们几乎无法察觉,因此看似这几个软件是同时运行,这种方式就叫做并发;
相对的,假如我的电脑是四核 CPU,那么就可以支持 4 个软件真正的同时运行,而不是快速交替使用,这种方式叫做并行。
在 Python 中,threading 和 multiprocessing 是两个用于实现并发和并行的模块。它们分别基于线程和进程,适用于不同的任务类型,下面是一个简单的例子。
threading 处理多线程并发:
# 并发:单核处理两个线程(交替执行)
import threading
def task():
print(threading.current_thread().name)
t1 = threading.Thread(target=task)
t2 = threading.Thread(target=task)
t1.start()
t2.start()
multiprocessing 实现多进程并行:
# 并行:需多核CPU支持(Python中需用多进程实现)
from multiprocessing import Process
def task():
print(Process().name)
p1 = Process(target=task)
p2 = Process(target=task)
p1.start()
p2.start()
1.3 同步 vs 互斥
同步(Synchronization):
同步是指多个线程或进程按照一定的顺序执行,避免数据依赖或不一致的问题,解决是谁先谁后的问题,其目的是保证任务按照正确的时间顺序执行。
**举个栗子🌰:**比如我们生产某个产品,必须要获取其原材料才能生成,先获得原材料再生产这样的顺序就是同步。
互斥(Mutual Exclusion):
互斥是指多个线程或进程访问共享资源时,必须保证同一时刻只有一个线程能够访问,从而避免数据竞争和不一致,主要目的是防止多个线程同时修改同一块数据,导致数据错误或竞争条件(Race Condition)。
**举个栗子🌰:**我们对打印机的使用是互斥的,一个任务完成后才进行另一个任务,而不是在一张纸上打印一行你的文档,再打印一行别人的文档。
我们在编写 Python 多线程脚本时,会遇到很多需要同步或者互斥的情况,那么这时候就需要某种机制来实现互斥和同步,我们主要使用 threading 模块中的 锁(Lock)、信号量(Semaphore) 和 事件(Event) 等。
1.3.1 互斥锁(Lock)
使用互斥锁,可以确保同一时刻只有一个线程可以访问共享资源,适用于多个线程竞争同一个资源的情况。
示例(使用 **threading.Lock**)
想象一下,某电商平台放出了 1 台低价iPhone 16 秒杀,成千上万的人都在疯狂抢购!如果不加锁,可能会出现 “超卖”,导致多人同时买到同一台手机的情况。为了防止这种情况,我们需要用 互斥锁(Lock) 确保每次只有一个人能成功下单。
import threading
import time
import random
# 共享资源(商品库存)
stock = 1 # 只剩 1 台 iPhone
lock = threading.Lock() # 创建互斥锁
def buy(buyer):
global stock # 需要修改全局变量
with lock: # 确保只有一个线程能操作库存
if stock > 0:
print(f"{buyer} 正在抢购 iPhone...")
time.sleep(random.uniform(0.1, 0.5)) # 模拟下单过程
stock -= 1
print(f"🎉 {buyer} 抢购成功!库存剩余: {stock}")
else:
print(f"😭 {buyer} 抢购失败,商品已经被抢光了!")
# 10 个人疯狂抢购
buyers = ["小明", "小红", "张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十"]
random.shuffle(buyers)
threads = []
for buyer in buyers:
t = threading.Thread(target=buy, args=(buyer,))
threads.append(t)
t.start()
# 等待所有线程执行完毕
for t in threads:
t.join()
print("抢购结束!")
运行结果(可能有所不同)
小红 正在抢购 iPhone...
🎉 小红 抢购成功!库存剩余: 0
小明 正在抢购 iPhone...
😭 小明 抢购失败,商品已经被抢光了!
张三 正在抢购 iPhone...
😭 张三 抢购失败,商品已经被抢光了!
...
最终,只有 1 个人 能抢到手机,其他人只能等下次补货!😂
1.3.2 线程同步(Condition)
使用threading.Condition可以处理线程间的依赖关系,比如解决生产者-消费者问题。
消费者必须等待生产者生产处产品后,才能消费,先后顺序的问题就是同步问题。
import threading
import time
condition = threading.Condition()
data = None
def producer():
global data
time.sleep(2)
with condition:
data = "生产的数据"
print("生产者:生产了数据")
condition.notify() # 通知消费者
def consumer():
global data
with condition:
print("消费者:等待数据...")
condition.wait() # 等待生产者通知
print(f"消费者:消费了数据 -> {data}")
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t2.start()
t1.start()
t1.join()
t2.join()
使用condition.wait() 让 消费者等待数据,直到生产者 **notify()** 生产完成。
在 Python 多线程编程中,如果涉及到共享资源,建议一定要使用锁,否则可能会出现互相竞争,导致数据错误!
2. Python多线程开发常用库
2.1 threading库(多线程)
threading是 python 标准库,基于线程实现并发,通过共享内存实现数据交互,但受 全局解释器锁(GIL) 限制,同一时刻仅允许一个线程执行 Python 字节码。
主要特点:
- 轻量级:线程创建和切换开销远小于进程。
- 共享内存:直接访问全局变量,数据交互方便。
- 适用性广:适合大多数非计算密集场景。
- GIL 限制:无法利用多核 CPU 进行并行计算。
- 调试复杂:线程安全问题(如竞态条件、死锁)需手动处理同步机制。
常用语法:
1. 创建一个线程
创建线程时你需要定义一个函数,这个函数就是线程要做的事情,然后用 threading.Thread 来创建线程对象,并启动它。
import threading
import time
def say_hello(name):
print(f"你好, {name}!")
time.sleep(2) # 模拟耗时操作
print(f"再见👋🏻, {name}!")
# 创建线程
thread = threading.Thread(target=say_hello, args=("凌小添",))
# 启动线程
thread.start()
# 主线程继续执行
print("主线程还在运行哦!")
在这个例子中:
say_hello是线程要执行的函数。threading.Thread(target=say_hello, args=("凌小添",))创建了一个线程对象,target是要执行的函数,args是传递给函数的参数。thread.start()启动线程,线程会开始执行say_hello函数。
2. 等待线程完成
有时候,主线程需要等待所有线程完成任务后再继续执行。可以用 join() 方法来实现。
thread.join() # 等待线程完成
print("所有线程都完成了!")
如果主线程不等待,可能会出现线程还没执行完,主线程就结束了的情况,用 join() 就能避免这个问题。
3. 线程同步与互斥
本部分的相关内容上文已经提到,此处不再赘述,我们知道threading 提供的一些工具就足够了。
2.2 multiprocessing库(多进程)
Python 标准库,基于进程实现并行,每个进程拥有独立内存空间,彻底绕过 GIL 限制,充分利用多核 CPU 的计算能力,适合 CPU 密集型任务。
特点:
- 并行:利用多核 CPU,突破 GIL 限制。
- 内存隔离:进程间内存独立,避免意外数据污染。
- 稳定性高:单个进程崩溃不会影响其他进程。
- 资源开销大:进程创建和通信(IPC)成本高。
- 数据交互复杂:需使用队列(
Queue)、管道(Pipe)或共享内存(Value/Array)进行进程间通信。
常用语法:
1. 创建进程
要创建一个进程,需要要定义一个函数,这个函数就是进程要做的事情。
然后用 multiprocessing.Process 来创建进程对象,并启动它。
import multiprocessing
import time
def worker(name):
print(f"Worker {name} started")
time.sleep(2) # 模拟耗时操作
print(f"Worker {name} finished")
# 创建进程
process1 = multiprocessing.Process(target=worker, args=("Alice",))
process2 = multiprocessing.Process(target=worker, args=("Bob",))
# 启动进程
process1.start()
process2.start()
# 等待进程完成
process1.join()
process2.join()
print("All processes completed!")
-
其中
multiprocessing.Process(target=worker, args=("Alice",))创建了一个进程对象 -
target是要执行的函数,args是传递给函数的参数。 -
process1.start()启动进程,进程会开始执行worker函数。 -
process1.join()等待进程完成。
2. 进程池(Pool)
如果你有很多任务需要并发执行,手动创建和管理进程会很麻烦。multiprocessing 提供了 Pool,可以更方便地管理进程。
import multiprocessing
import time
def worker(x):
print(f"Processing {x}")
time.sleep(1) # 模拟耗时操作
return x * x
if __name__ == "__main__":
# 创建进程池
with multiprocessing.Pool(processes=4) as pool:
# 使用 map 方法并发执行任务
results = pool.map(worker, range(10))
print("Results:", results)
- 其中
worker是进程要执行的函数。 multiprocessing.Pool(processes=4)创建了一个进程池,processes参数指定了进程池中进程的数量。pool.map(worker, range(10))将任务分配给进程池中的进程并发执行,map方法会自动将结果收集到一个列表中。
3. 进程间通信
多进程的一个挑战是进程之间不能直接共享内存,但 multiprocessing 提供了一些工具来实现进程间通信:
**Queue**(队列):用于在进程之间传递消息。**Pipe**(管道):用于在两个进程之间传递数据。**Value**和**Array**:用于在进程之间共享简单的数据。
例如我们使用队列来实现经典的生产者消费者问题:
import multiprocessing
import time
def producer(queue):
for i in range(5):
print(f"Producing {i}")
queue.put(i)
time.sleep(1)
def consumer(queue):
while True:
item = queue.get()
if item is None:
break
print(f"Consuming {item}")
time.sleep(1)
if __name__ == "__main__":
# 创建一个队列
queue = multiprocessing.Queue()
# 创建生产者和消费者进程
producer_process = multiprocessing.Process(target=producer, args=(queue,))
consumer_process = multiprocessing.Process(target=consumer, args=(queue,))
# 启动进程
producer_process.start()
consumer_process.start()
# 等待生产者完成
producer_process.join()
# 向队列中放入一个 None,通知消费者结束
queue.put(None)
# 等待消费者完成
consumer_process.join()
print("All done!")
producer是生产者进程,负责生成数据并放入队列。consumer是消费者进程,负责从队列中取出数据并处理。multiprocessing.Queue()创建了一个队列,用于在生产者和消费者之间传递数据。
2.3 concurrent.futures库(多线程+多进程)
concurrent.futures 是 Python 标准库中的 高层并发框架,它简化了多线程 (ThreadPoolExecutor) 和多进程 (ProcessPoolExecutor) 的管理,让我们可以用更优雅的方式执行并发任务,无需手动管理线程或进程。
特点:
- 统一 API:
submit()和map()方法通用于线程和进程。 - 自动管理:通过
with语句自动回收资源,避免泄露。 - 异步支持:通过
Future对象实现非阻塞结果获取。 - 灵活性高:轻松在 “线程池” 和 “进程池” 间切换。
- 控制粒度粗:隐藏底层细节,难以实现复杂同步逻辑。
- 性能折衷:高层抽象可能带来轻微性能损失。
我们刚刚已经了解了多线程(**threading**)和多进程(**multiprocessing****)**进行并发编程。但它们都有缺点:
threading.Thread: 需要手动创建、管理多个线程,代码较繁琐。multiprocessing.Process: 创建进程比线程消耗更大,管理起来不方便。
而 **concurrent.futures** 提供了更高层的抽象,用线程池/进程池来自动管理线程和进程,让并发编程变得更简单。
核心语法:
concurrent.futures 主要有 两个核心执行器(Executor):
ThreadPoolExecutor:基于 多线程 并发(适用于 I/O 密集型任务,如爬虫、文件下载)。ProcessPoolExecutor:基于 多进程 并行(适用于 CPU 密集型任务,如数据计算、图像处理)。
| 方法 | 作用 |
submit(fn, *args, **kwargs) | 提交一个任务给线程/进程池,返回 Future对象 |
map(func, iterable) | 类似 map()函数,按顺序并行执行多个任务 |
shutdown(wait=True) | 关闭线程/进程池,释放资源 |
as_completed(fs) | 迭代获取已完成的任务 |
Future.result() | 获取 Future任务的返回值 |
1. 我们先来看一个多线程操作:**ThreadPoolExecutor**
多线程适用于 I/O 密集型 任务,比如爬取网页、处理 I/O 读写、数据库查询等。
假设我们要下载 1000 个网页,就可以开启多线程加速处理,下列为伪代码(表示逻辑,无法直接运行)
import concurrent.futures
import time
import random
def download_page(url):
print(f"正在下载 {url}...")
time.sleep(random.uniform(1, 3)) # 模拟网络延迟
return f"{url} 下载完成"
# 要爬取的网页列表
urls = [f"https://example.com/page{i}" for i in range(1, 6)]
# 创建线程池,最大线程数 max_workers=3。
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
#submit() 提交下载任务,返回 Future 对象。
future_to_url = {executor.submit(download_page, url): url for url in urls}
#as_completed() 逐个获取已完成的任务,这样可以处理更快完成的任务,而不是按提交顺序等待。
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
result = future.result()
except Exception as exc:
print(f"{url} 发生错误: {exc}")
else:
print(f"{result}")
2.我们再来看看多进程操作**ProcessPoolExecutor**
多进程适用于 CPU 密集型 任务,比如大规模数据处理、科学计算、图像处理等。
假如我们要计算 1 到 100 每个数字的平方值,使用多进程,充分利用 CPU 加速。
import concurrent.futures
def compute_square(n):
return n * n
numbers = list(range(1, 11)) # 计算 1~10 的平方
# 使用 ProcessPoolExecutor 创建进程池,max_workers=4。
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
#使用 map() 并行执行任务,无需手动 submit(),比 for 循环高效。
results = executor.map(compute_square, numbers)
print(list(results)) # [1, 4, 9, 16, ..., 10000]
**concurrent.futures** 适用场景
| 适用情况 | ThreadPoolExecutor | ProcessPoolExecutor |
| I/O 密集型(爬虫、文件下载、数据库操作) | ✅ 适合 | ❌ 不适合 |
| CPU 密集型(大规模计算、数据处理) | ❌ 不适合 | ✅ 适合 |
| 任务较小,管理方便 | ✅ 适合 | ❌ 适合但代价大 |
| Python GIL 限制影响 | ❌ 受影响 | ✅ 不受影响 |
2.4 这么多库,怎么选?
当我们具体去写代码时:
- 优先选择
**concurrent.futures**库,简单易用,适用于大多数场景,尤其是需要快速实现并发的任务(如批量下载、数据处理)。
当需要精细控制时:
- 如需自定义线程同步逻辑(如复杂锁机制),选择
threading - 如需彻底绕过 GIL 执行计算任务,利用多核 CPU 时,选择
multiprocessing
| 库 | threading | multiprocessing | concurrent.futures |
| 并发模型 | 多线程(共享内存) | 多进程(独立内存) | 线程池 / 进程池封装 |
| 适用场景 | I/O 密集型任务(爬虫、文件 I/O) | CPU 密集型任务(科学计算、数据处理) | 通用场景,快速开发 |
| 资源开销 | 🟢 低 | 🔴 高 | 🟡 中等 |
| 代码复杂度 | 较高(需手动处理) | 高(需处理进程通信) | 低(高层接口封装) |
| 调试难度 | 🔴高 | 🟡 中 | 🟢 低 |
3. 综合案例:多线程词频统计工具
说明:
我们已经学习了多线程编程,那么应用到一个具体的案例里面来试试!
具体要求:
- 支持格式:自动处理文件夹内的
.txt、.doc、.docx、.md文件。 - 编码兼容:自动处理常见中文编码(UTF-8、GBK)。
- 多线程加速:每个文件分配独立线程处理。
- 词频统计:输出前十项高频词,过滤停用词和短词。
完整代码
import os
import jieba
from collections import defaultdict
from docx import Document # 需安装 python-docx
from concurrent.futures import ThreadPoolExecutor, as_completed
# -------------------- 配置参数 --------------------
STOPWORDS = {"的", "在", "了", "是", "和", "我", "你"} # 自定义停用词
FILE_TYPES = ('.txt', '.doc', '.docx', '.md') # 支持的文件类型
NUM_THREADS = 4 # 线程数
TOP_N = 10 # 显示高频词数量
# -------------------- 文件读取函数 --------------------
def read_file(file_path):
""" 根据文件类型调用对应的读取方法 """
ext = os.path.splitext(file_path)[1].lower()
try:
if ext == '.txt' or ext == '.md':
for encoding in ['utf-8', 'gbk']:
try:
with open(file_path, 'r', encoding=encoding) as f:
return f.read()
except UnicodeDecodeError:
continue
return ""
elif ext in ('.doc', '.docx'):
doc = Document(file_path)
return '\n'.join([para.text for para in doc.paragraphs])
else:
return ""
except Exception as e:
print(f"读取文件失败: {file_path} ({str(e)})")
return ""
# -------------------- 处理文件的函数 --------------------
def process_file(file_path):
""" 处理单个文件,返回局部词频统计 """
content = read_file(file_path)
words = jieba.cut(content)
local_count = defaultdict(int)
for word in words:
if word not in STOPWORDS and len(word) > 1:
local_count[word] += 1
return local_count
# -------------------- 主函数 --------------------
def analyze_folder(folder_path):
jieba.initialize()
file_paths = []
# 遍历文件夹获取文件列表
for root, _, files in os.walk(folder_path):
for file in files:
if file.lower().endswith(FILE_TYPES):
file_paths.append(os.path.join(root, file))
global_word_count = defaultdict(int)
# 使用 ThreadPoolExecutor 进行并行处理
with ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
future_to_file = {executor.submit(process_file, file): file for file in file_paths}
for future in as_completed(future_to_file):
local_count = future.result()
for word, count in local_count.items():
global_word_count[word] += count
# 输出结果
sorted_words = sorted(global_word_count.items(), key=lambda x: -x[1])
print("\n=== 高频关键词Top-{} ===".format(TOP_N))
for word, count in sorted_words[:TOP_N]:
print(f"{word}: {count}次")
if __name__ == "__main__":
target_folder = input("请输入要分析的文件夹路径: ")
analyze_folder(target_folder)
- 运行示例:
请输入要分析的文件夹路径: ./documents
=== 高频关键词Top-10 ===
人工智能: 892次
大数据: 765次
云计算: 632次
区块链: 587次
机器学习: 543次
深度学习: 487次
网络安全: 432次
物联网: 398次
数字化转型: 365次
算法: 321次
性能对比(测试10,000个文件)
| 方法 | 耗时 | CPU利用率 |
| 单线程 | 58秒 | 15%~20% |
| 多线程(4) | 16秒 | 70%~90% |