Python 中多线程(multi-threading)与多进程(multi-processing)的比较

多线程和多进程是 Python 中两种实现多任务的不同策略,二者都可以在特定的场景下在一定程度上提高程序的运行速度、性能以及吞吐,但二者的运行机制却有很大的差别。

在 Python 中,多线程以并发 (concurrent)的方式运行,适用于 I/O 密集型 任务的场景;多进程以并行 (parallelism)的方式运行,适用于计算密集型任务的场景。虽然多进程也可以用于 I/O 密集型任务的场景,但这会导致部分 CPU 性能的浪费,且进程的开销也比线程要高。而多线程用于计算密集型的任务场景则相当于这些任务串行执行。

⒈ GIL(Global Interpreter Lock)

⑴ GIL 介绍

GIL 是一种互斥锁机制,其目的是为了确保在同一时间只有一个线程可以运行,进而确保了在同一时间只有一个线程可以访问/操作内存。但这同时也阻止了 Python 多线程对多核处理器的充分利用。

⑵ 为什么引入 GIL

Python 通过引用计数的方式进行垃圾回收。当一个对象的引用计数降为 0 时,该对象会被垃圾回收,其所占用的内存空间会被释放。

Python 复制代码
import sys


a = []
b = a
print(sys.getrefcount(a)) # 3

上例中,对象 [] 被变量 ab 以及函数 sys.getrefcount() 的参数同时引用,故结果为 3

如果允许同一时间有多个线程同时运行,那么这些线程同时修改对象的引用计数可能会出现竞争条件,最终导致对象的引用计数错误,进而导致程序崩溃(引用计数过早的降为 0)或内存泄漏。

Python 复制代码
import threading


count = 0


def accumulator():
    global count
    for i in range(0, 100000):
        count += 1


threads = []

for i in range(0, 100):
    threads.append(threading.Thread(target=accumulator))

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

print(count) # 8752687

GIL 只是确保同一时间只有一个线程在执行,但并不一定能阻止竞争条件

⑶ GIL 带来的影响

GIL 使得 Python 多线程无法充分利用多核处理器的优势,进而使得多线程无法适用于计算密集型的场景。

GIL 可能会在 Python 3.13 中设置为可选项

⒉ 多线程

在 Python 中,由于 GIL 机制的限制,同一时间只能有一个线程在执行,这就决定了 Python 的多线程只会对 I/O 密集型任务的性能有显著的提升。

在 I/O 密集型的任务场景中,当一个线程因为等待 I/O 被阻塞时,系统可以将当前线程挂起而执行其他线程,这样可以充分利用 CPU 资源,缩短程序的总体运行时间,提升性能。

Python 复制代码
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import matplotlib.pyplot as plt
from random import choice
from string import ascii_letters
import time


def multi_threading(func, args, workers):
    with ThreadPoolExecutor(max_workers=workers) as ex:
        res = ex.map(func, args)

    return list(res)


def create_text():
    text = ''.join(choice(ascii_letters) for i in range(10**7))

    return text


def io_bound(text: str, base: float = 0):
    start = time.time() - base
    f = open('letters.txt', 'wt', encoding='utf-8')
    f.write(text)
    end = time.time() - base

    return start, end


def visualize_performance(times: list, workers: list):
    figure, axe = plt.subplots(layout='constrained')
    figure.suptitle('io bound tasks: more threads, less time')
    axe.set_xlabel('number of threads')
    axe.set_ylabel("time used (s)")
    axe.bar(workers, times)
    plt.show()


texts = [create_text() for i in range(16)]
workers_num = [1, 2, 4, 8, 16]
times = []
for num in workers_num:
    start = time.time()
    res = multi_threading(io_bound, texts, num)
    end = time.time()
    times.append(end - start)

visualize_performance(times, ['1', '2', '4', '8', '16'])

上述代码通过将一个随机生成的包含 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 0 7 10^7 </math>107 个字符的字符串写入文本文件 16 次来模拟 I/O 密集型任务的场景。在程序运行过程中,整体的运行时间会随着线程数量的增加而下降。

Python 复制代码
def multi_threading(func, args, workers):
    start_time = time.time()
    with ThreadPoolExecutor(max_workers=workers) as ex:
        res = ex.map(func, args, [start_time] * len(args))

    return list(res)
    

def visualize_mechanism(times: list, task_num: int):
    figure, axe = plt.subplots(layout='constrained')
    figure.suptitle('io bound tasks with %s threads' % task_num)
    axe.set_xlabel('seconds')
    axe.set_ylabel('tasks')
    widths = [t[1] - t[0] for t in times]
    lefts = [t[0] for t in times]
    axe.barh(range(len(times)), widths, left=lefts)
    plt.show()


texts = [create_text() for i in range(16)]
workers_num = [1, 2, 4, 8, 16]
times = []

for num in workers_num:
    res = multi_threading(io_bound, texts, num)
    visualize_mechanism(res, num)

对之前的代码进行适当的修改,查看多线程的运行机制。当只有一个线程来执行上述文件写入操作时,所有的 16 次文件写入会依次执行,每一次写入都只有等到上一次写入彻底完成后才能开始执行。

而在多线程模式下,上述的文件写入操作则会并发执行。

对于计算密集型的任务,随着线程数量的增加,任务的执行速度和性能并没有提升。

Python 复制代码
def cpu_bound(base: float = 0):
    start = time.time() - base
    count = 0
    for i in range(0, 10**5):
        count += 1

    end = time.time() - base

    return start, end

⒊ 多进程

Python 中的多进程充分利用了多核处理器的优势,使得多任务可以并行执行。并行执行的任务数越多,程序运行的越快。Python 多进程模式下,每个进程都有各自独立的解释器(interpreter),各个进程的内存空间也相互独立,所以 GIL 机制并不会成为多进程的瓶颈。

Python 复制代码
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import matplotlib.pyplot as plt
from random import choice
from string import ascii_letters
import time
from math import cos, pi


def multi_processing(func, workers):
    with ProcessPoolExecutor(max_workers=workers) as ex:
        res = ex.map(func, [0] * 16)

    return list(res)


def cpu_bound(base: float = 0):
    start = time.time() - base
    count = 0
    for i in range(0, 10**5):
        count += cos(i * pi)

    end = time.time() - base

    return start, end


workers_num = [1, 2, 4, 8, 16]
times = []

for num in workers_num:
    start = time.time()
    res = multi_processing(cpu_bound, num)
    end = time.time()

    times.append(end - start)

visualize_performance(times, ['1', '2', '4', '8', '16'])

上述代码通过进行 16 次大量数学计算模拟计算密集型任务场景。随着进程数量的增加,刚开始程序的运行会越来越快。但当进程数量超过机器内核数量时(4),程序运行反而会因为进程的切换而变慢。

当所有的 16 次计算任务都通过一个进程进行时,这些任务会串行执行;但随着进程数量的增加,同时执行的任务也会增加;当进程数量超过机器的内核数量时,任务可能会因为进程切换而交错执行。

使用多进程处理 I/O 密集型的任务,程序运行速度会随着进程数量增加而变快,但同样,当进程数量超过内核数量时,程序的运行速度会变慢。

相关推荐
zzb15807 分钟前
Agent案例-智能文档问答助手
java·人工智能·笔记·python
HP-Patience14 分钟前
【Python爬虫常见错误】- AJAX动态加载数据爬取
爬虫·python·ajax
青瓷程序设计19 分钟前
【基于 YOLO的咖啡豆果实成熟度检测系统】+ Python+算法模型+目标检测+2026原创
python·算法·yolo
天才测试猿19 分钟前
Python接口自动化测试之Token详解及应用
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·接口测试
童园管理札记28 分钟前
2026实测|GPT-4.5+Agent智能体:3小时搭建企业级客服系统,附完整源码与部署教程(二)
人工智能·python
:mnong34 分钟前
附图报价系统设计分析3
python·openvino
AmyLin_200135 分钟前
【pdf2md-2:关键核心】PDF 转 Markdown 技术拆解:两阶段流水线、四级标题检测与段落智能合并
windows·python·pdf·pip·pdf2md
薛不痒38 分钟前
Llamafactory的使用(1)
人工智能·python·llama
不喝水的鱼儿39 分钟前
KT Qwen3.5-35B-A3B 记录
java·前端·python
小陈工1 小时前
Python Web开发入门(三):配置文件管理与环境变量最佳实践
开发语言·jvm·数据库·python·oracle·性能优化·开源