Python多线程与多进程深度解析:GIL机制、性能对比及适用场景

拾光
拾光
发布时间:2026-10-13| 6人浏览过
原创

本文深入解析Python中多线程与多进程的实现机制,重点剖析GIL(全局解释器锁)对CPU密集型任务的影响。通过对比实验展示线程与进程在运算耗时上的显著差异,明确多线程适用于IO密集型场景,而多进程更适合CPU密集型任务,为开发者提供清晰的并发编程选型依据。

前言与概述

前言

在技术学习中,理解核心概念与原理至关重要,细节往往可以通过实践逐步完善。掌握的关键在于深入理解,借助具体实例和实际操作来感性体会概念,能显著提升学习效果。本文将通过具体案例,简要介绍Python中多线程与多进程的实现方式,后续还将探讨进程间与线程间的通信机制。

Python多线程基础

Python多线程

Python标准库提供了thread和threading两个模块以支持线程操作。在Python 3中,前者已不再受支持,后者作为更高层级的封装库成为主流选择。下文将以threading模块为例进行阐述。

创建线程

在Python中实现线程主要有两种途径:

1. 实例化一个threading.Thread对象,并将一个初始化函数(initial function)作为线程执行的入口传入;

2. 继承threading.Thread类,并重写其run方法;

方式1:创建threading.Thread对象


import threading
import time
def tstart(arg):
 time.sleep(0.5)
 print("%s running...." % arg)
if __name__ == '__main__':
 t1 = threading.Thread(target=tstart, args=('This is thread 1',))
 t2 = threading.Thread(target=tstart, args=('This is thread 2',))
 t1.start()
 t2.start()
 print("This is main function")

运行结果如下:


This is main function
This is thread 2 running....
This is thread 1 running....

方式2:继承threading.Thread并重写run方法


import threading
import time
class CustomThread(threading.Thread):
 def __init__(self, thread_name):
  # step 1: call base __init__ function
  super(CustomThread, self).__init__(name=thread_name)
  self._tname = thread_name
 def run(self):
  # step 2: overide run function
  time.sleep(0.5)
  print("This is %s running...." % self._tname)
if __name__ == "__main__":
 t1 = CustomThread("thread 1")
 t2 = CustomThread("thread 2")
 t1.start()
 t2.start()
 print("This is main function")

其执行结果与方式1相同。

threading.Thread

上述两种方法在本质上都是直接或间接地使用了threading.Thread类。

threading.Thread(group=None, target=None, name=None, args=(), kwargs={})

将上述两种创建线程的方式关联起来看:


import threading
import time
class CustomThread(threading.Thread):
 def __init__(self, thread_name, target = None):
  # step 1: call base __init__ function
  super(CustomThread, self).__init__(name=thread_name, target=target, args = (thread_name,))
  self._tname = thread_name
 def run(self):
  # step 2: overide run function
  # time.sleep(0.5)
  # print("This is %s running....@run" % self._tname)
  super(CustomThread, self).run()
def target(arg):
 time.sleep(0.5)
 print("This is %s running....@target" % arg)
if __name__ == "__main__":
 t1 = CustomThread("thread 1", target)
 t2 = CustomThread("thread 2", target)
 t1.start()
 t2.start()
 print("This is main function")

运行结果如下:


This is main function
This is thread 1 running....@target
This is thread 2 running....@target

上述代码说明了以下几点:

1. 无论采用哪种方式创建线程,指定的参数最终都会传递给threading.Thread类;

2. 传递给线程的目标函数是在基类Thread的run方法中被调用的,除非run方法被重写。

关于threading模块的更多属性和方法可参考官方文档,此处重点介绍threading.Thread对象的相关方法。

以下是threading.Thread提供的线程对象方法和属性:

  • start():创建线程后调用start启动线程,此时线程进入等待CPU调度状态,为执行run函数做准备;
  • run():线程执行的入口函数,其内部会调用用户指定的target函数,或执行被重写的run函数;
  • join([timeout]):阻塞调用该方法的线程,直到被调用的线程执行完毕或超时。通常在主线程中调用此方法,以等待其他子线程执行完成;
  • name、getName()&setName():用于获取或设置线程名称;
  • ident:整数类型的线程标识符,在调用start之前为None;
  • isAlive()、is_alive():在start执行之后到run执行完毕之前返回True;
  • daemon、isDaemon()&setDaemon():用于设置或获取守护线程状态;

以上方法主要用于创建线程后的管理和信息获取。

多线程执行与同步

多线程执行

在主线程中创建多个子线程后,它们之间默认没有协作和同步机制。除主线程外,每个子线程均从run方法开始执行,直至执行完毕。

join

我们可以利用join方法使主线程阻塞,从而等待其创建的子线程执行完成。


import threading
import time
def tstart(arg):
 print("%s running....at: %s" % (arg,time.time()))
 time.sleep(1)
 print("%s is finished! at: %s" % (arg,time.time()))
if __name__ == '__main__':
 t1 = threading.Thread(target=tstart, args=('This is thread 1',))
 t1.start()
 t1.join() # 当前线程阻塞,等待t1线程执行完成
 print("This is main function at:%s" % time.time())

运行结果如下:


This is thread 1 running....at: 1564906617.43
This is thread 1 is finished! at: 1564906618.43
This is main function at:1564906618.43

若不加任何限制,当主线程执行完毕后,当前程序并不会立即结束,必须等待所有子线程结束后,进程才会终止。

若将上述程序中的t1.join()移除,执行结果如下:


This is thread 1 running....at: 1564906769.52
This is main function at:1564906769.52
This is thread 1 is finished! at: 1564906770.52

可以通过将创建的线程指定为守护线程(daemon),这样当主线程执行完毕后,会立即终止未执行完的子线程,进而结束整个程序。

deamon守护线程


import threading
import time
def tstart(arg):
  print("%s running....at: %s" % (arg,time.time()))
  time.sleep(1)
  print("%s is finished! at: %s" % (arg,time.time()))
if __name__ == '__main__':
  t1 = threading.Thread(target=tstart, args=('This is thread 1',))
  t1.setDaemon(True)
  t1.start()
  # t1.join()  # 当前线程阻塞,等待t1线程执行完成
  print("This is main function at:%s" % time.time())

运行结果如下:


This is thread 1 running....at: 1564906847.85
This is main function at:1564906847.85

Python多进程实现

Python多进程

与用于创建多线程的threading模块不同,Python提供了multiprocessing模块来创建多进程。首先查看创建进程的两种方式。

The multiprocessing package mostly replicates the API of the threading module. —— python doc

创建进程

创建进程的方式与创建线程类似:

1. 实例化一个multiprocessing.Process对象,并传入一个初始化函数(initial function)作为新进程的执行入口;

2. 继承multiprocessing.Process,并重写run方法;

方式1:


from multiprocessing import Process 
import os, time
def pstart(name):
  # time.sleep(0.1)
  print("Process name: %s, pid: %s "%(name, os.getpid()))
if __name__ == "__main__": 
  subproc = Process(target=pstart, args=('subprocess',)) 
  subproc.start() 
  subproc.join()
  print("subprocess pid: %s"%subproc.pid)
  print("current process pid: %s" % os.getpid())

运行结果如下:


Process name: subprocess, pid: 4888 
subprocess pid: 4888
current process pid: 9912

方式2:


from multiprocessing import Process 
import os, time
class CustomProcess(Process):
  def __init__(self, p_name, target=None):
    # step 1: call base __init__ function()
    super(CustomProcess, self).__init__(name=p_name, target=target, args=(p_name,))

  def run(self):
    # step 2:
    # time.sleep(0.1)
    print("Custom Process name: %s, pid: %s "%(self.name, os.getpid()))
if __name__ == '__main__':
  p1 = CustomProcess("process_1")
  p1.start()
  p1.join()
  print("subprocess pid: %s"%p1.pid)
  print("current process pid: %s" % os.getpid())

此处值得思考:如果像多线程那样存在一个全局变量share_data,不同进程同时访问该变量会有问题吗?

由于每个进程拥有独立的内存地址空间且相互隔离,因此不同进程看到的share_data是不同的,分别位于各自的地址空间中,同时访问不会引发冲突。这一点需要特别注意。

Subprocess模块

既然提到多进程,顺便介绍另一种创建进程的方式。

Python提供了Subprocess模块,允许在程序执行过程中调用外部程序。

例如,我们可以在Python程序中打开记事本、cmd,或在特定时间关机:


>>> import subprocess
>>> subprocess.Popen(['cmd'])
<subprocess.Popen object at 0x0339F550>
>>> subprocess.Popen(['notepad'])
<subprocess.Popen object at 0x03262B70>
>>> subprocess.Popen(['shutdown', '-p'])

或者使用ping测试网络连通性:


>>> res = subprocess.Popen(['ping', 'www.cnblogs.com'], stdout=subprocess.PIPE).communicate()[0]
>>> print res
正在 Ping www.cnblogs.com [101.37.113.127] 具有 32 字节的数据:
来自 101.37.113.127 的回复: 字节=32 时间=1ms TTL=91 来自 101.37.113.127 的回复: 字节=32 时间=1ms TTL=91
来自 101.37.113.127 的回复: 字节=32 时间=1ms TTL=91
来自 101.37.113.127 的回复: 字节=32 时间=1ms TTL=91

101.37.113.127 的 Ping 统计信息:
  数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
  最短 = 1ms,最长 = 1ms,平均 = 1ms

线程与进程性能对比

Python多线程与多进程比较

首先来看两个例子:

开启两个Python线程分别执行一亿次加一操作,以及单独使用一个线程执行相同操作:


def tstart(arg):
  var = 0
  for i in xrange(100000000):
    var += 1

if __name__ == '__main__':
  t1 = threading.Thread(target=tstart, args=('This is thread 1',))
  t2 = threading.Thread(target=tstart, args=('This is thread 2',))
  start_time = time.time()
  t1.start()
  t2.start()
  t1.join()
  t2.join()
  print("Two thread cost time: %s" % (time.time() - start_time))
  start_time = time.time()
  tstart("This is thread 0")
  print("Main thread cost time: %s" % (time.time() - start_time))

运行结果如下:


Two thread cost time: 20.6570000648
Main thread cost time: 2.52800011635

上述例子中,如果只开启t1和t2中的一个线程,其运行时间与主线程基本一致。这将在后文解释原因。

使用两个进程执行上述操作:


def pstart(arg):
  var = 0
  for i in xrange(100000000):
    var += 1
if __name__ == '__main__':
  p1 = Process(target = pstart, args = ("1", ))
  p2 = Process(target = pstart, args = ("2", ))
  start_time = time.time()
  p1.start()
  p2.start()
  p1.join()
  p2.join()
  print("Two process cost time: %s" % (time.time() - start_time))
  start_time = time.time()
  pstart("0")
  print("Current process cost time: %s" % (time.time() - start_time))

运行结果如下:


Two process cost time: 2.91599988937
Current process cost time: 2.52400016785

对比分析

双进程并行执行与单进程执行相同的运算代码,耗时基本相同,双进程耗时略多,这可能是由于进程创建和销毁涉及系统调用,带来了额外的时间开销。

然而对于Python线程,双线程并行执行的耗时远高于单线程,效率相差近10倍。若将两个并行线程改为串行执行,即:


t1.start()
  t1.join()
  t2.start()
  t2.join()
  #Two thread cost time: 5.12199997902
  #Main thread cost time: 2.54200005531

可以看到,三个线程串行执行时,每个线程的执行时间基本相同。

其本质原因在于双线程是并发执行,而非真正的并行执行。这归因于GIL锁。

GIL锁机制解析

GIL锁

讨论Python多线程时,不得不提GIL(Global Interpreter Lock,全局解释器锁)。这是目前主流的Python解释器CPython为保证数据安全而实现的一种锁机制。无论进程中有多少线程,只有获取GIL锁的线程才能在CPU上运行,即使是在多核处理器上。对于单个进程而言,无论有多少线程,任一时刻只有一个线程在执行。因此,对于CPU密集型线程,其效率不仅不高,反而可能更低。Python多线程更适用于IO密集型程序。对于确实需要并行运行的程序,建议考虑多进程。

线程对锁的争夺、CPU对线程的调度以及线程间的切换均会产生时间开销。

线程与进程的核心区别

线程与进程区别

以下简要比较线程与进程:

  • 进程是资源分配的基本单位,线程是CPU执行和调度的基本单位;
  • 通信/同步方式:
    • 进程:
      • 通信方式:管道,FIFO,消息队列,信号,共享内存,socket,stream流;
      • 同步方式:PV信号量,管程
    • 线程:
      • 同步方式:互斥锁,递归锁,条件变量,信号量
      • 通信方式:位于同一进程的线程共享进程资源,因此线程间没有类似于进程间用于数据传递的通信方式,线程间的通信主要用于线程同步。
  • CPU上真正执行的是线程,线程比进程轻量,其切换和调度代价比进程小;
  • 线程间对于共享的进程数据需考虑线程安全问题。由于进程之间是隔离的,拥有独立的内存空间资源,相对更安全,只能通过上述列出的IPC(Inter-Process Communication)进行数据传输;
  • 系统由一个个进程组成,每个进程包含代码段、数据段、堆空间和栈空间,以及操作系统共享部分,具有等待、就绪和运行三种状态;
  • 一个进程可以包含多个线程,线程之间共享进程的资源(文件描述符、全局变量、堆空间等),寄存器变量和栈空间等是线程私有的;
  • 操作系统中一个进程挂掉不会影响其他进程。如果一个进程中的某个线程挂掉且OS对线程的支持是多对一模型,则会导致当前进程挂掉;
  • 如果CPU和系统支持多线程与多进程,多个进程并行执行的同时,每个进程中的线程也可以并行执行,这样才能最大限度地发挥硬件性能;

上下文切换与选型建议

线程和进程的上下文切换

进程切换涉及众多因素,包括将寄存器内容保存到任务状态段TSS、切换页表、堆栈等。简单来说可分为两步:

页全局目录切换,使CPU到新进程的线性地址空间寻址; 切换内核态堆栈和硬件上下文,硬件上下文包含CPU寄存器的内容,存放在TSS中; 线程运行于进程地址空间,切换过程不涉及空间变换,仅涉及第二步;

使用多线程还是多进程?

CPU密集型:程序需要占用CPU进行大量的运算和数据处理;

I/O密集型:程序中需要频繁进行I/O操作,例如网络中socket数据传输和读取等;

由于Python多线程并非并行执行,因此更适合I/O密集型程序;多进程并行执行则适用于CPU密集型程序;

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。