Python多线程编程实战:守护线程、锁机制与GIL详解

江屿
江屿
发布时间:2026-10-13 | 5人浏览过
原创

本文深入解析Python多线程编程,涵盖threading模块的核心用法。详细介绍三种Thread实例化方式、守护线程的设置与行为,以及通过Lock和with语句实现线程同步。同时剖析GIL对多线程性能的影响,区分I/O密集型与计算密集型应用的适用场景,并提供完整代码示例。

Python多线程概述与守护线程机制

本文通过具体实例详细讲解Python中多线程的使用方法,旨在帮助开发者掌握并发编程的核心技巧。

threading模块支持守护线程(Daemon Threads)。这类线程通常用于后台服务,例如等待客户端请求。

若将某个线程设置为守护线程,当主进程退出时,无需等待该线程执行完毕即可直接结束进程。

当主线程准备退出且无需等待特定子线程完成时,可为这些子线程设置守护标记。具体操作是在启动线程前执行赋值语句:thread.daemon = True。检查线程是否处于守护状态,只需验证该属性值即可。

Python程序将在所有非守护线程结束后才退出,即只有当不存在存活的非守护线程时,进程才会终止。

Thread类的三种实例化方法

使用thread模块

以下是三种实例化Thread类的方法,通常推荐使用第一种或第三种方案。

  • 创建Thread实例,并传入一个普通函数作为目标。

import threading
from time import sleep, ctime
loops = [3, 2, 1, 1, 1]
def loop(i, nsec):
  print(f'start loop {i} at: {ctime()}')
  sleep(nsec)
  print(f'end loop {i} at: {ctime()}')
def main():
  print('start at', ctime())
  threads = []
  nloops = range(len(loops))
  for i in nloops:
    t = threading.Thread(target=loop, args=(i, loops[i]))
    threads.append(t)
  for i in nloops: # start threads
    threads[i].start()
  for i in nloops: # wait for all
    threads[i].join() # threads to finish
  print(f'all done at: {ctime()}')
if __name__ == '__main__':
  main()

所有线程分配完成后,需调用每个线程的start()方法启动执行,而非在创建时立即运行。相比管理锁(分配、获取、释放及检查状态),此处只需为每个线程调用join()方法。join()会阻塞直到线程结束,或达到指定的超时时间。使用join()比无限循环等待锁释放更清晰(这也是自旋锁的概念来源)。

  • 创建Thread实例,并传入一个可调用类的实例作为目标。

import threading
from time import sleep, ctime
# 创建 Thread 的实例,传给它一个可调用的类实例
loops = [3, 2, 1, 1, 1]
class ThreadFunc(object):
  def __init__(self, func, args, name=''):
    self.name = name
    self.func = func
    self.args = args
  def __call__(self):
    self.func(*self.args)
def loop(i, nsec):
  print(f'start loop {i} at: {ctime()}')
  sleep(nsec)
  print(f'end loop {i} at: {ctime()}')
def main():
  print('start at', ctime())
  threads = []
  nloops = range(len(loops))
  for i in nloops:
    t = threading.Thread(target=ThreadFunc(loop, (i, loops[i]), loop.__name__))
    threads.append(t)
  for i in nloops: # start threads
    threads[i].start()
  for i in nloops: # wait for all
    threads[i].join() # threads to finish
  print(f'all done at: {ctime()}')
if __name__ == '__main__':
  main()
  • 继承Thread子类,并创建该子类的实例。

import threading
from time import sleep, ctime
# 创建 Thread 的实例,传给它一个可调用的类实例
# 子类的构造函数必须先调用其基类的构造函数
# 特殊方法__call__()在 子类中必须要写为 run()
loops = [3, 2, 1, 1, 1]
class MyThread(threading.Thread):
  def __init__(self, func, args, name=''):
    threading.Thread.__init__(self)
    self.name = name
    self.func = func
    self.args = args
  def run(self):
    self.func(*self.args)
def loop(i, nsec):
  print(f'start loop {i} at: {ctime()}')
  sleep(nsec)
  print(f'end loop {i} at: {ctime()}')
def main():
  print('start at', ctime())
  threads = []
  nloops = range(len(loops))
  for i in nloops:
    t = MyThread(loop, (i, loops[i]), loop.__name__)
    threads.append(t)
  for i in nloops: # start threads
    threads[i].start()
  for i in nloops: # wait for all
    threads[i].join() # threads to finish
  print(f'all done at: {ctime()}')
if __name__ == '__main__':
  main()

线程锁(Lock)的使用与同步

使用锁

与Java类似,Python也具备锁机制,且创建和使用都非常简便。

在多线程代码中,某些特定函数或代码块(如修改数据库、更新文件或其他可能产生竞态条件的情况)不应被多个线程同时执行。

锁仅有两种状态:锁定和未锁定,并只支持两个操作:获取锁(acquire)和释放锁(release)。

常规锁调用方式如下。


# 加载线程的锁对象
lock = threading.Lock()
# 获取锁
lock.acquire()
# ...代码
# 释放锁
lock.release()

更简洁的方法是使用with关键字,其功能与上述代码相同。


# 加载线程的锁对象
lock = threading.Lock()
with lock :
  #...代码

完整示例代码。


import threading
from time import sleep, ctime
lock = threading.Lock()
def a():
  lock.acquire()
  for x in range(5):
    print(f'a:{str(x)}')
    sleep(0.01)
  lock.release()
def b():
  lock.acquire()
  for x in range(5):
    print(f'a:{str(x)}')
    sleep(0.01)
  lock.release()
threading.Thread(target=a).start()
threading.Thread(target=b).start()

Thread对象属性、方法及模块函数参考

相关属性和方法

  • Thread对象的属性

属性 描述
name 线程名
ident 线程的标识符
daemon 布尔标志,表示这个线程是否是守护线程
 
  • Thread对象的方法

方法 描述
init(group=None, tatget=None, name=None, args=(), kwargs ={}, verbose=None, daemon=None) 实例化一个线程对象,需要有一个可调用的 target,以及其参数 args 或 kwargs。还可以传递 name 或 group 参数,不过后者还未实现。此 外 , verbose 标 志 也 是 可 接 受 的 。 而 daemon 的 值 将 会 设 定 thread.daemon 属性/标志
start() 开始执行该线程
run() 定义线程功能的方法(通常在子类中被应用开发者重写)
join (timeout=None) 直至启动的线程终止之前一直挂起;除非给出了 timeout(秒),否则 会一直阻塞
is_alive() 布尔标志,表示这个线程是否还存活
  • threading模块的其他函数

函数 描述
start() 开始执行该线程
active_count() 当前活动的 Thread 对象个数
enumerate() 返回当前活动的 Thread 对象列表
settrace(func) 为所有线程设置一个 trace 函数
setprofile (func) 为所有线程设置一个 profile 函数
stack_size(size=0) 返回新创建线程的栈大小;或为后续创建的线程设定栈的大小 为 size
Lock() 加载线程的锁对象,是一个基本的锁对象,一次只能一个锁定,其余锁请求,需等待锁释放后才能获取,对象有acquire()和release()方法
RLock() 多重锁,在同一线程中可用被多次acquire。如果使用RLock,那么acquire和release必须成对出现,调用了n次acquire锁请求,则必须调用n次的release才能在线程中释放锁对象

GIL限制与多线程性能分析

后记

在Python多线程环境下,每个线程的执行流程如下:

1、获取GIL(全局解释器锁); 2、执行代码直到遇到sleep或Python虚拟机将其挂起; 3、释放GIL。

通常多线程是有益的,但由于Python GIL的限制,多线程更适合I/O密集型应用(I/O操作会释放GIL,允许更多并发),而非计算密集型应用。对于计算密集型任务,为实现更好的并行性,应使用多进程以利用CPU的其他核心。

注意:在多核环境下,多线程性能可能比单核更差。原因是单核下多线程每次释放GIL后,唤醒的线程能无缝获取GIL继续执行;但在多核下,CPU0释放GIL后,其他CPU上的线程会竞争GIL,但GIL可能立即被CPU0重新获取,导致其他CPU上被唤醒的线程在等待切换时间后重新进入待调度状态,造成线程颠簸(thrashing),从而降低效率。

更多关于Python相关内容感兴趣的读者可查看本站专题:《Python进程与线程操作技巧总结》、《Python数据结构与算法教程》、《Python函数使用技巧总结》、《Python字符串操作技巧汇总》、《Python入门与进阶经典教程》、《Python+MySQL数据库程序设计入门教程》及《Python常见数据库操作技巧汇总》。

希望本文所述的Python程序设计知识对大家有所帮助。

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表