AIGC标识 Python 多线程与多进程(测试工程师版)

 

所有代码用 python3 文件名.py 即可运行。


目录

  1. 为什么测试工程师需要学并发?
  2. 基础概念:进程 vs 线程
  3. Python 的特殊性:GIL 是什么?
  4. 多线程实战:threading 模块
  5. 多进程实战:multiprocessing 模块
  6. 线程安全与锁
  7. 线程池与进程池:concurrent.futures
  8. 测试场景实战
  9. 常见坑与面试高频题
  10. 面试问答

一、为什么测试工程师需要学并发?

先看几个测试中的真实场景:

场景问题并发方案
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 "❌"
        print(f"{status} {result['endpoint']}: {result.get('status', result.get('error'))}")

场景2:带超时控制的并发测试

from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
import time

def test_with_timeout(url, timeout=3):
    """带超时的接口测试"""
    try:
        resp = requests.get(url, timeout=timeout)
        return {"url": url, "status": resp.status_code, "success": True}
    except requests.Timeout:
        return {"url": url, "error": "超时", "success": False}
    except Exception as e:
        return {"url": url, "error": str(e), "success": False}

urls = [
    "https://httpbin.org/delay/1",   # 1 秒返回
    "https://httpbin.org/delay/2",   # 2 秒返回
    "https://httpbin.org/delay/5",   # 5 秒返回(会触发超时)
    "https://httpbin.org/delay/3",   # 3 秒返回
]

with ThreadPoolExecutor(max_workers=4) as executor:
    futures = {executor.submit(test_with_timeout, url): url for url in urls}

    for future in as_completed(futures):
        result = future.result()
        if result["success"]:
            print(f"{result['url']}{result['status']}")
        else:
            print(f"{result['url']}{result['error']}")

场景3:测试数据库写入性能(多进程加速)

假设有 10000 条测试数据要入库

from concurrent.futures import ProcessPoolExecutor
import sqlite3
import time

# 模拟批量插入
def batch_insert(batch_data):
    """批量插入测试数据"""
    conn = sqlite3.connect(":memory:")  # 实际场景连真实数据库
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS test_data (id INTEGER, value TEXT)")

    for item in batch_data:
        cursor.execute("INSERT INTO test_data VALUES (?, ?)", item)

    conn.commit()
    count = cursor.execute("SELECT COUNT(*) FROM test_data").fetchone()[0]
    conn.close()
    return f"插入了 {count} 条数据"

if __name__ == "__main__":
    # 构造 10000 条测试数据
    all_data = [(i, f"data_{i}") for i in range(10000)]

    # 分成 4 批,每批 2500 条
    batch_size = 2500
    batches = [all_data[i:i+batch_size] for i in range(0, len(all_data), batch_size)]

    start = time.time()
    with ProcessPoolExecutor(max_workers=4) as executor:
        results = executor.map(batch_insert, batches)

    print(f"✅ 完成: {list(results)}")
    print(f"⏱️ 耗时: {time.time() - start:.2f}s")

场景4:并发执行 pytest 用例(思路)

实际项目用 pytest-xdist 插件即可,这里是理解其原理

import subprocess
from concurrent.futures import ThreadPoolExecutor
import time

def run_test_file(test_file):
    """执行单个测试文件"""
    result = subprocess.run(
        ["python3", "-m", "pytest", test_file, "-v", "--tb=short"],
        capture_output=True, text=True
    )
    passed = "passed" in result.stdout
    return {"file": test_file, "passed": passed, "output": result.stdout[-200:]}

# 模拟测试文件列表
test_files = [
    "tests/test_login.py",
    "tests/test_order.py",
    "tests/test_payment.py",
    "tests/test_refund.py",
]

with ThreadPoolExecutor(max_workers=2) as executor:
    futures = {executor.submit(run_test_file, f): f for f in test_files}
    for future in as_completed(futures):
        result = future.result()
        icon = "✅" if result["passed"] else "❌"
        print(f"{icon} {result['file']}")

# 实际项目不要这样干,直接用:
# python3 -m pytest tests/ -n auto  (需要 pip install pytest-xdist)

场景5:高并发秒杀接口压测

from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
import time

def seckill_request(user_id, product_id):
    """模拟秒杀请求"""
    start = time.time()
    try:
        resp = requests.post(
            "https://httpbin.org/post",
            json={"user_id": user_id, "product_id": product_id, "action": "buy"},
            timeout=5
        )
        duration = time.time() - start
        return {"user": user_id, "status": resp.status_code, "duration": f"{duration:.3f}s"}
    except Exception as e:
        return {"user": user_id, "error": str(e)}

# 模拟 50 个用户同时抢购
users = [f"user_{i}" for i in range(50)]

with ThreadPoolExecutor(max_workers=50) as executor:
    futures = {executor.submit(seckill_request, u, "iphone_99"): u for u in users}

    success = 0
    failed = 0
    for future in as_completed(futures):
        result = future.result()
        if result.get("status") == 200:
            success += 1
        else:
            failed += 1
            print(f"{result['user']}: {result.get('error', '失败')}")

    print(f"\n📊 结果: 成功={success}, 失败={failed}")

九、常见坑与面试高频题

坑1:多线程中的异常不会被自动捕获

import threading

def bad_func():
    raise ValueError("出错了")

t = threading.Thread(target=bad_func)
t.start()
t.join()
# ❌ 异常不会影响主线程,静默消失!

# ✅ 正确做法:自己包装异常
def safe_func():
    try:
        raise ValueError("出错了")
    except Exception as e:
        print(f"捕获异常: {e}")

坑2:共享变量的可见性问题

import threading

flag = False

def waiter():
    global flag
    while not flag:  # 可能永远看不到 flag 的变化
        pass
    print("检测到 flag 变化")

def setter():
    global flag
    flag = True

# ✅ 用 Event 代替轮询
event = threading.Event()

def waiter2():
    event.wait()  # 等待事件
    print("检测到事件")

def setter2():
    event.set()  # 触发事件

坑3:死锁

import threading

lock_a = threading.Lock()
lock_b = threading.Lock()

def task1():
    with lock_a:
        print("task1 获取 lock_a")
        with lock_b:
            print("task1 获取 lock_b")

def task2():
    with lock_b:          # 顺序反了!
        print("task2 获取 lock_b")
        with lock_a:
            print("task2 获取 lock_a")

# 可能死锁!解决:所有线程按相同顺序获取锁

# ✅ 改进:定义锁获取顺序
LOCKS = [lock_a, lock_b]  # 统一顺序
def safe_task(lock_ids):
    for i in sorted(lock_ids):  # 按固定顺序获取
        LOCKS[i].acquire()

坑4:进程间的数据是独立的

from multiprocessing import Process
import os

shared_list = []  # ❌ 每个进程有自己独立的副本

def worker():
    shared_list.append(os.getpid())
    print(f"进程 {os.getpid()}: {shared_list}")

if __name__ == "__main__":
    processes = [Process(target=worker) for _ in range(3)]
    for p in processes: p.start()
    for p in processes: p.join()
    print(f"主进程: {shared_list}")  # 仍然是 []

十、面试问答

Q1:Python 多线程能利用多核 CPU 吗?

面试官想听到的关键词:GIL、CPU密集型 vs I/O密集型

回答

不能直接利用。Python 的 CPython 解释器有 GIL(全局解释器锁),它确保同一时刻只有一个线程在执行 Python 字节码。

所以 CPU 密集型任务用多线程不会加速,反而有切换开销。

但对于 I/O 密集型任务(网络请求、文件读写等),线程在等待 I/O 时会释放 GIL,其他线程可以执行,所以多线程能显著提升效率。

作为测试工程师,我们绝大多数场景是 I/O 密集型的(发 HTTP 请求测试接口),所以多线程非常实用。如果要做大量计算,应该用多进程或者 ProcessPoolExecutor

Q2:说说你对进程和线程的理解,测试中怎么选?

面试官想听到的关键词:资源隔离、共享内存、并发场景

回答

理解

  • 进程是资源分配的最小单位,有独立的内存空间
  • 线程是 CPU 调度的最小单位,同一进程内的线程共享内存

测试中的选择

  • 接口测试/UI 测试 → 多线程(I/O 密集,线程池即可)
  • 大量测试数据生成/解析 → 多进程(CPU 密集,需要多核)
  • 需要隔离环境的测试(如每个 worker 独立数据库)→ 多进程
  • 实时监控多个服务 → 多线程+守护线程

实际项目中最常用的是 concurrent.futures.ThreadPoolExecutor一个 with 语句就能搞定并发,而且线程池复用避免了频繁创建线程的开销。

Q3:什么是线程安全?测试中如何处理?

面试官想听到的关键词:竞态条件、锁、原子操作

回答

线程安全指多个线程同时访问共享资源时,程序仍然能正确执行。

如果不做保护,会出现竞态条件(Race Condition),比如多个线程同时往列表里写结果,可能导致数据丢失。

测试中的处理方案

  1. threading.Lock 保护共享资源(如测试结果收集器)
  2. 尽量让每个线程只处理自己的数据,最后再合并(无共享则无竞争)
  3. 使用线程安全的容器(如 queue.Queue
  4. concurrent.futures 的返回值获取结果,避免直接操作共享变量

实际项目中,我一般维护一个带锁的 TestResultCollector 类,统一收集并发测试的结果。

Q4:写个代码:用多线程并发调用3个接口,全部完成后汇总结果

面试官想看的:ThreadPoolExecutor + as_completed 的掌握程度

回答

from concurrent.futures import ThreadPoolExecutor, as_completed
import requests

def call_api(url, payload=None):
    try:
        if payload:
            resp = requests.post(url, json=payload, timeout=10)
        else:
            resp = requests.get(url, timeout=10)
        return {"url": url, "status": resp.status_code, "data": resp.json()}
    except Exception as e:
        return {"url": url, "error": str(e)}

apis = [
    ("https://httpbin.org/get", None),
    ("https://httpbin.org/post", {"key": "value"}),
    ("https://httpbin.org/delay/1", None)
]

with ThreadPoolExecutor(max_workers=3) as executor:
    futures = {executor.submit(call_api, url, payload): url for url, payload in apis}
    results = [future.result() for future in as_completed(futures)]

# 全部完成后汇总
for r in results:
    print(r)

Q5:多进程如何共享数据?

面试官想听到的关键词:multiprocessing.Queue、Manager、Pipe

回答

多进程内存相互独立,不能像线程那样直接读写共享变量。 标准方案有:

  1. Queue:最常用,线程安全的 FIFO 队列
  2. Pipe:两个进程间的双向/单向通信通道
  3. Manager:提供进程间共享的列表、字典等容器(底层用 Socket 实现)
  4. Value/Array:共享内存中的简单类型

测试中最常用的是 Queue,比如多个 worker 进程产生测试数据,主进程从 Queue 中收集写入数据库。


快速参考:常用 API 一览

# --- threading ---
threading.Thread(target=func, args=(1,), daemon=True)
threading.Lock()
threading.RLock()
threading.Event()
threading.Semaphore(3)           # 限制同时访问的线程数
threading.current_thread().name  # 当前线程名称

# --- multiprocessing ---
multiprocessing.Process(target=func, args=(1,))
multiprocessing.Pool(4)          # 进程池(旧 API)
multiprocessing.Queue()
multiprocessing.Manager()

# --- concurrent.futures (推荐) ---
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed

# 创建
with ThreadPoolExecutor(max_workers=4) as executor:

# 提交单个任务
future = executor.submit(func, arg1, arg2)
result = future.result(timeout=5)  # 获取结果,可设超时

# 批量提交(按提交顺序返回)
results = executor.map(func, [arg1, arg2, arg3])

# 按完成顺序处理
futures = [executor.submit(func, i) for i in items]
for f in as_completed(futures):
    result = f.result()

posted on 2026-07-25 00:38  fengZQ  阅读(7)  评论(0)    收藏  举报

导航