Python 多线程与多进程(测试工程师版)
所有代码用
python3 文件名.py即可运行。
目录
- 为什么测试工程师需要学并发?
- 基础概念:进程 vs 线程
- Python 的特殊性:GIL 是什么?
- 多线程实战:threading 模块
- 多进程实战:multiprocessing 模块
- 线程安全与锁
- 线程池与进程池:concurrent.futures
- 测试场景实战
- 常见坑与面试高频题
- 面试问答
一、为什么测试工程师需要学并发?
先看几个测试中的真实场景:
| 场景 | 问题 | 并发方案 |
|---|---|---|
| 1000 个接口用例串行跑 | 跑完要 30 分钟 | 多线程/多进程并行跑,缩短到 3 分钟 |
| 测试高并发抢购接口 | 单线程发请求没压力 | 多线程同时发请求,模拟真实并发 |
| 同时监控 10 个服务健康状态 | 依次检查太慢 | 多线程并发检查 |
| 构造 10000 条测试数据 | for 循环写入太慢 | 多进程并行生成 |
核心认知:并发编程不是"炫技",是测试工程师提效和模拟真实场景的必备技能。
二、基础概念:进程 vs 线程
通俗理解
进程 = 一家独立的餐厅
├── 有自己的厨房、餐具、员工(独立资源)
├── 和其他餐厅互不干扰
└── 开新餐厅成本高
线程 = 餐厅里的一个服务员
├── 共用餐厅的厨房和餐具(共享资源)
├── 两个服务员可以同时服务不同客人(并发)
└── 招新服务员成本低
进程(Process)
- 独立性:每个进程有独立的内存空间,互不干扰
- 资源开销:大(创建进程需要分配独立内存)
- 通信方式:进程间通信(IPC),如 Queue、Pipe
- 适用场景:CPU 密集型任务(计算、数据处理)
线程(Thread)
- 共享性:同一进程内的线程共享内存空间
- 资源开销:小(轻量级)
- 通信方式:直接读写共享变量(但需要加锁)
- 适用场景:I/O 密集型任务(网络请求、文件读写)
关键对比表
| 维度 | 进程 | 线程 |
|---|---|---|
| 内存空间 | 独立 | 共享 |
| 创建速度 | 慢(ms 级) | 快(μs 级) |
| 通信难度 | 较复杂 | 简单(共享变量) |
| 安全性 | 高(天然隔离) | 低(需要锁保护) |
| Python 中受 GIL 影响 | 不受 | 受(CPU 密集场景) |
三、Python 的特殊性:GIL 是什么?
这是 Python 面试必问题。
GIL(Global Interpreter Lock,全局解释器锁)
一句话:CPython 解释器同一时刻只允许一个线程执行 Python 字节码。
这意味着什么?
# 演示 GIL 的影响
import threading
import time
def count_down(n):
while n > 0:
n -= 1
# 单线程
start = time.time()
count_down(50000000)
print(f"单线程耗时: {time.time() - start:.2f}s")
# 多线程(受 GIL 限制,不会更快)
t1 = threading.Thread(target=count_down, args=(25000000,))
t2 = threading.Thread(target=count_down, args=(25000000,))
start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
print(f"多线程耗时: {time.time() - start:.2f}s")
# 输出:多线程 ≈ 单线程 × 1.x(甚至更慢,因为线程切换开销)
核心结论
| 任务类型 | 举例 | 多线程 | 多进程 |
|---|---|---|---|
| CPU 密集型 | 大量计算、数据解析 | ❌ GIL 限制,不加速 | ✅ 充分利用多核 |
| I/O 密集型 | HTTP 请求、文件读写、数据库查询 | ✅ 大大加速 | ✅ 但浪费资源 |
测试工程师视角:绝大多数测试场景是 I/O 密集型(发请求、读写数据库),所以多线程在测试中非常实用。
四、多线程实战:threading 模块
4.1 创建和启动线程
import threading
import time
def request_api(user_id):
"""模拟调用接口"""
print(f"[线程 {threading.current_thread().name}] 开始请求用户 {user_id}")
time.sleep(2) # 模拟网络延迟
print(f"[线程 {threading.current_thread().name}] 用户 {user_id} 请求完成")
# --- 串行执行 ---
start = time.time()
for i in range(5):
request_api(i)
print(f"串行耗时: {time.time() - start:.2f}s")
print("\n" + "="*50 + "\n")
# --- 多线程执行 ---
start = time.time()
threads = []
for i in range(5):
t = threading.Thread(target=request_api, args=(i,))
threads.append(t)
t.start()
# 等待所有线程结束
for t in threads:
t.join()
print(f"多线程耗时: {time.time() - start:.2f}s")
运行结果预期:
- 串行:约 10 秒(5 × 2秒)
- 多线程:约 2 秒(所有请求并行)
4.2 守护线程(Daemon)
import threading
import time
def background_task():
"""后台任务(如定时检查服务状态)"""
while True:
print("[守护线程] 检查服务健康状态...")
time.sleep(1)
# daemon=True:主线程结束,守护线程自动退出
daemon = threading.Thread(target=background_task, daemon=True)
daemon.start()
time.sleep(3)
print("主线程结束,守护线程自动退出")
4.3 线程传参方式
import threading
# 方式1:args 元组传参
t1 = threading.Thread(target=func, args=(1, 2, 3))
# 方式2:kwargs 字典传参
t2 = threading.Thread(target=func, kwargs={"a": 1, "b": 2})
# 方式3:继承 Thread 类(适合复杂逻辑)
class TestThread(threading.Thread):
def __init__(self, url, timeout=10):
super().__init__()
self.url = url
self.timeout = timeout
self.response = None
def run(self):
"""线程入口方法"""
import requests
try:
self.response = requests.get(self.url, timeout=self.timeout)
except Exception as e:
self.response = str(e)
t3 = TestThread("http://localhost:8080/health")
t3.start()
t3.join()
print(t3.response)
五、多进程实战:multiprocessing 模块
5.1 创建和启动进程
import multiprocessing
import time
def cpu_intensive_task(n):
"""CPU 密集型计算"""
count = 0
for i in range(n):
count += i ** 2
print(f"进程 {multiprocessing.current_process().name} 计算完成")
return count
if __name__ == "__main__": # Windows 必需,macOS/Linux 建议加
# 串行
start = time.time()
for _ in range(4):
cpu_intensive_task(50000000)
print(f"串行耗时: {time.time() - start:.2f}s")
print()
# 多进程(利用多核 CPU)
start = time.time()
processes = []
for _ in range(4):
p = multiprocessing.Process(target=cpu_intensive_task, args=(50000000,))
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"多进程耗时: {time.time() - start:.2f}s")
注意:
if __name__ == "__main__"是 multiprocessing 的安全保护,防止无限递归创建进程。
5.2 获取进程返回值
直接 target 函数的返回值无法获取,需要用以下方式:
import multiprocessing
def square(n):
return n * n
if __name__ == "__main__":
with multiprocessing.Pool(4) as pool:
results = pool.map(square, [1, 2, 3, 4, 5])
print(results) # [1, 4, 9, 16, 25]
六、线程安全与锁
6.1 竞态条件(Race Condition)
多个线程同时修改共享变量,导致结果不正确:
import threading
counter = 0
def increment():
global counter
for _ in range(100000):
temp = counter # 读取
temp += 1 # 修改
counter = temp # 写回
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"期望: 1000000, 实际: {counter}")
# 输出可能远小于 1000000(因为线程间互相干扰)
6.2 使用 Lock 解决
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock: # 加锁:同一时刻只有一个线程能执行这段代码
temp = counter
temp += 1
counter = temp
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"期望: 1000000, 实际: {counter}")
# 正确输出 1000000
6.3 RLck(可重入锁)
import threading
lock = threading.RLock() # 可重入锁
def func1():
with lock:
print("func1 获取锁")
func2() # 同一线程可以再次获取
def func2():
with lock: # 如果是普通 Lock,这里会死锁
print("func2 获取锁")
# RLock 允许同一线程多次获取,普通 Lock 不允许
6.4 测试中的锁使用原则
# ✅ 好:用于保护共享资源(如测试结果收集器)
import threading
class TestResultCollector:
def __init__(self):
self.results = []
self._lock = threading.Lock()
def add_result(self, case_name, status, duration):
with self._lock:
self.results.append({
"case": case_name,
"status": status,
"duration": duration
})
def get_summary(self):
with self._lock:
passed = sum(1 for r in self.results if r["status"] == "PASS")
failed = sum(1 for r in self.results if r["status"] == "FAIL")
return {"total": len(self.results), "passed": passed, "failed": failed}
# ❌ 不好:对每个 HTTP 请求加锁(失去了并发的意义)
lock = threading.Lock()
def bad_request(url):
with lock: # 加锁后变成串行
return requests.get(url)
七、线程池与进程池:concurrent.futures
这是 Python 3.2+ 提供的高级并发 API,推荐使用,比手动管理线程/进程更安全、更简洁。
7.1 ThreadPoolExecutor(线程池)
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def test_login(username):
"""模拟登录测试"""
time.sleep(1) # 模拟网络延迟
# 实际场景:requests.post("http://api/login", json={"user": username})
return f"{username} 登录成功"
# 方式1:submit + as_completed(灵活)
start = time.time()
with ThreadPoolExecutor(max_workers=5) as executor:
# 提交所有任务
futures = {executor.submit(test_login, f"user_{i}"): i for i in range(10)}
# 按完成顺序获取结果
for future in as_completed(futures):
result = future.result()
print(f"完成: {result}")
print(f"总耗时: {time.time() - start:.2f}s")
print()
# 方式2:map(简洁,按提交顺序返回)
start = time.time()
with ThreadPoolExecutor(max_workers=5) as executor:
results = executor.map(test_login, [f"user_{i}" for i in range(10)])
for result in results:
print(f"完成: {result}")
print(f"总耗时: {time.time() - start:.2f}s")
7.2 ProcessPoolExecutor(进程池)
from concurrent.futures import ProcessPoolExecutor
import time
def heavy_calculation(n):
"""耗时计算"""
total = 0
for i in range(n):
total += i ** 3
return total
if __name__ == "__main__":
start = time.time()
with ProcessPoolExecutor(max_workers=4) as executor:
results = executor.map(heavy_calculation, [5000000, 5000000, 5000000, 5000000])
for r in results:
print(f"结果: {r}")
print(f"耗时: {time.time() - start:.2f}s")
7.3 如何选择
| 场景 | 推荐方案 |
|---|---|
| 并发发 HTTP 请求(接口测试) | ThreadPoolExecutor |
| 并发读写数据库 | ThreadPoolExecutor |
| 大批量数据计算/解析 | ProcessPoolExecutor |
| 同时处理文件上传 + 数据解析 | 外部 ThreadPool,内部 ProcessPool(分层) |
八、测试场景实战
场景1:并发接口测试(最常用)
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
BASE_URL = "https://httpbin.org" # 公共测试 API
def test_get_endpoint(endpoint):
"""并发测试 GET 接口"""
url = f"{BASE_URL}/{endpoint}"
start = time.time()
try:
resp = requests.get(url, timeout=5)
duration = time.time() - start
return {
"endpoint": endpoint,
"status": resp.status_code,
"duration": f"{duration:.3f}s",
"success": resp.ok
}
except Exception as e:
return {"endpoint": endpoint, "error": str(e), "success": False}
# 并发测试多个接口
endpoints = ["get", "post", "put", "delete", "patch", "headers", "ip", "user-agent"]
with ThreadPoolExecutor(max_workers=5) as executor:
futures = {executor.submit(test_get_endpoint, e): e for e in endpoints}
for future in as_completed(futures):
result = future.result()
status = "✅" if result["success"] else "❌"