Python多线程并发实战:从基础到守护与阻塞线程优化

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

本文深入解析Python多线程并发编程,涵盖threading模块基础使用、多线程创建与启动实例。重点探讨500线程并发性能测试及优化策略,详细讲解守护线程(setDaemon)与阻塞线程(join)的区别与应用场景,通过代码示例演示如何处理主线程与子线程的生命周期管理,帮助开发者掌握高效并发控制技巧。

Python多线程基础与单线程执行

单线程执行

Python内置了thread和threading两个模块。其中thread是底层原生模块,而threading是对thread的封装与扩展,提供了更高级的功能和更好的易用性。因此,在实际开发中,我们通常直接使用threading模块来实现并发测试和逻辑。

实例

创建并启动一个单线程


import threading
def myTestFunc():
print("我是一个函数")
t = threading.Thread(target=myTestFunc) # 创建一个线程
t.start() # 启动线程

执行结果


C:\Python36\python.exe D:/MyThreading/myThread.py
我是一个线程函数
Process finished with exit code 0

单线程的执行效果与直接调用函数无异,核心区别在于线程机制允许同时运行多个任务,而普通函数调用是串行的。通过线程对象,我们可以实现并发执行逻辑。

多线程创建与并发性能测试

多线程执行

前述内容介绍了单线程的使用方式。实现多线程的关键在于通过循环结构批量创建线程对象,并依次启动它们以执行指定的线程函数。

实例


import threading
from datetime import datetime
def thread_func(): # 线程函数
  print('我是一个线程函数', datetime.now())
def many_thread():
  threads = []
  for _ in range(10): # 循环创建10个线程
    t = threading.Thread(target=thread_func)
    threads.append(t)
  for t in threads: # 循环启动10个线程
    t.start()
if __name__ == '__main__':
  many_thread()

执行结果


C:\Python36\python.exe D:/MyThreading/manythread.py
我是一个线程函数 2019-06-23 16:54:58.205146
我是一个线程函数 2019-06-23 16:54:58.205146
我是一个线程函数 2019-06-23 16:54:58.206159
我是一个线程函数 2019-06-23 16:54:58.206159
我是一个线程函数 2019-06-23 16:54:58.206159
我是一个线程函数 2019-06-23 16:54:58.207139
我是一个线程函数 2019-06-23 16:54:58.207139
我是一个线程函数 2019-06-23 16:54:58.207139
我是一个线程函数 2019-06-23 16:54:58.208150
我是一个线程函数 2019-06-23 16:54:58.208150
Process finished with exit code 0

上述代码通过循环创建了10个线程并依次启动。需要注意的是,Python中的并发并非绝对意义上的“同时”处理。由于线程启动存在微小的时间差,执行结果中会体现细微的时间差异,尽管这种差异通常可以忽略不计。然而,当线程数量极大时,这种启动顺序导致的差异会显著放大。为了验证这一点,我们将线程数量提升至500个,观察程序的整体执行耗时。

实例


import threading
from datetime import datetime
def thread_func(): # 线程函数
print('我是一个线程函数', datetime.now())
def many_thread():
threads = []
for _ in range(500): # 循环创建500个线程
t = threading.Thread(target=thread_func)
threads.append(t)
for t in threads: # 循环启动500个线程
t.start()
if __name__ == '__main__':
start = datetime.today().now()
many_thread()
duration = datetime.today().now() - start
print(duration)

执行结果


0:00:00.111657
Process finished with exit code 0

测试结果显示,500个线程的总执行耗时约为0.11秒。

多线程并发优化策略

针对上述启动顺序导致的并发差异问题,我们可以采用优化策略:创建25个线程,每个线程内部循环执行20次目标函数。这样,当主程序启动下一个线程时,前一个线程已在内部循环中执行任务,从而大幅减少了线程间启动的时间差异,使并发效果更接近真正的并行处理。

优化


import threading
from datetime import datetime
def thread_func(): # 线程函数
print('我是一个线程函数', datetime.now())
def execute_func():
for _ in range(20):
thread_func()
def many_thread():
start = datetime.now()
threads = []
for _ in range(25): # 循环创建500个线程
t = threading.Thread(target=execute_func)
threads.append(t)
for t in threads: # 循环启动500个线程
t.start()
duration = datetime.now() - start
print(duration)
if __name__ == '__main__':
many_thread()

输出结果(仅看程序执行间隔)


0:00:00.014959
Process finished with exit code 0

优化后的方案在500次并发任务中仅耗时约0.014秒,相比未优化的500个独立线程方案,速度提升了数倍。若线程函数的单次执行时间较长,这种优化带来的性能提升将更加显著。因此,在进行大规模并发测试时,推荐使用这种“线程内循环”的优化模式,其并发效果更为稳定和接近同步。

守护线程机制与应用

守护线程

多线程编程中另一个核心概念是守护线程。在深入之前,需明确主线程与子线程的关系:前文创建的线程均为主线程main()的子线程,即先启动主线程,再由主线程派生子线程执行具体任务。

所谓守护线程,是指当主线程执行完毕后,无论子线程是否执行完毕,所有子线程都将被强制关闭。默认情况下,Python不设置守护线程,此时主线程结束后会等待所有子线程执行完毕,程序才会最终退出。

然而,这种默认行为存在弊端:如果子线程陷入死循环或长时间阻塞等待,主线程虽已结束打印,但程序仍会因等待子线程而无限挂起。为了直观展示这一现象,我们将线程函数修改为循环10次,每次睡眠2秒,以观察更明显的效果。


import threading
from datetime import datetime
import time
def thread_func(): # 线程函数
   time.sleep(2)
i = 0
while(i < 11):
print(datetime.now())
i += 1
def many_thread():
threads = []
for _ in range(10): # 循环创建500个线程
t = threading.Thread(target=thread_func)
threads.append(t)
for t in threads: # 循环启动500个线程
t.start()
if __name__ == '__main__':
many_thread()
print("thread end")

执行结果


C:\Python36\python.exe D:/MyThreading/manythread.py
thread end
2019-06-23 19:08:00.468612
2019-06-23 19:08:00.468612
2019-06-23 19:08:00.468612
2019-06-23 19:08:00.468612
2019-06-23 19:08:00.468612
2019-06-23 19:08:00.468612
2019-06-23 19:08:00.468612
2019-06-23 19:08:00.468612
2019-06-23 19:08:00.468612
2019-06-23 19:08:00.468612
2019-06-23 19:08:00.468612
2019-06-23 19:08:00.469559
2019-06-23 19:08:00.469559
2019-06-23 19:08:00.469559
2019-06-23 19:08:00.469559
2019-06-23 19:08:00.469559
2019-06-23 19:08:00.469559
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.470556
2019-06-23 19:08:00.471554
2019-06-23 19:08:00.471554
2019-06-23 19:08:00.471554
2019-06-23 19:08:00.471554
2019-06-23 19:08:00.471554
2019-06-23 19:08:00.471554
2019-06-23 19:08:00.471554
2019-06-23 19:08:00.471554
2019-06-23 19:08:00.471554
2019-06-23 19:08:00.471554
2019-06-23 19:08:00.471554
2019-06-23 19:08:00.471554
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.472557
2019-06-23 19:08:00.473548
2019-06-23 19:08:00.473548
2019-06-23 19:08:00.473548
2019-06-23 19:08:00.473548
2019-06-23 19:08:00.473548
2019-06-23 19:08:00.473548
2019-06-23 19:08:00.473548
2019-06-23 19:08:00.473548
2019-06-23 19:08:00.473548
2019-06-23 19:08:00.473548
2019-06-23 19:08:00.473548
2019-06-23 19:08:00.473548
2019-06-23 19:08:00.473548
2019-06-23 19:08:00.474545
2019-06-23 19:08:00.474545
2019-06-23 19:08:00.474545
2019-06-23 19:08:00.474545
2019-06-23 19:08:00.474545
2019-06-23 19:08:00.474545
2019-06-23 19:08:00.474545
2019-06-23 19:08:00.475552
2019-06-23 19:08:00.475552
2019-06-23 19:08:00.475552
2019-06-23 19:08:00.475552
2019-06-23 19:08:00.475552
2019-06-23 19:08:00.475552
2019-06-23 19:08:00.475552
2019-06-23 19:08:00.475552
2019-06-23 19:08:00.475552
2019-06-23 19:08:00.476548
2019-06-23 19:08:00.476548
2019-06-23 19:08:00.476548
2019-06-23 19:08:00.476548
2019-06-23 19:08:00.476548
2019-06-23 19:08:00.476548
2019-06-23 19:08:00.476548
2019-06-23 19:08:00.476548
2019-06-23 19:08:00.476548
2019-06-23 19:08:00.476548
2019-06-23 19:08:00.477546
2019-06-23 19:08:00.477546
2019-06-23 19:08:00.477546
2019-06-23 19:08:00.477546
2019-06-23 19:08:00.477546
2019-06-23 19:08:00.477546
2019-06-23 19:08:00.477546
2019-06-23 19:08:00.477546
2019-06-23 19:08:00.477546
2019-06-23 19:08:00.477546
2019-06-23 19:08:00.477546
2019-06-23 19:08:00.477546
Process finished with exit code 0

从执行结果可见,主线程打印“thread end”后(即主线程逻辑结束),子线程仍在继续执行并打印时间戳,并未随主线程的结束而终止。

接下来,我们通过setDaemon方法将子线程设置为守护线程。我们将循环改为死循环,再次观察输出结果(注意:setDaemon必须在start之前调用)。


import threading
from datetime import datetime
def thread_func(): # 线程函数
i = 0
while(1):
print(datetime.now())
i += 1
def many_thread():
threads = []
for _ in range(10): # 循环创建500个线程
t = threading.Thread(target=thread_func)
threads.append(t)
t.setDaemon(True) # 给每个子线程添加守护线程
for t in threads: # 循环启动500个线程
t.start()

if __name__ == '__main__':
many_thread()
print("thread end")

输出结果


2019-06-23 19:12:35.564539
2019-06-23 19:12:35.564539
2019-06-23 19:12:35.564539
2019-06-23 19:12:35.564539
2019-06-23 19:12:35.564539
2019-06-23 19:12:35.564539
2019-06-23 19:12:35.565529
2019-06-23 19:12:35.565529
2019-06-23 19:12:35.565529
thread end
Process finished with exit code 0

结果显示,主线程结束后,子线程也随之立即关闭,未再无限循环。这种方式看似“暴力”,实则非常实用。例如,当子线程发送网络请求但未收到响应时,若无限等待会导致资源浪费或程序卡死。此时,守护线程能强制关闭子线程,解决了主线程与子线程生命周期同步的问题。

阻塞线程与join方法详解

阻塞线程

除了守护线程,是否还有其他方法解决上述子线程未结束导致程序挂起的问题?答案是肯定的,即使用阻塞线程。通过join()方法,可以让主线程等待子线程执行完毕后再继续执行,最后统一关闭所有子线程。这与守护线程“主线程结束即杀子线程”的逻辑不同,join()确保了子线程的完整执行。我们在代码中为子线程添加join()(注意:join必须在start之后调用)。


import threading
from datetime import datetime
import time
def thread_func(): # 线程函数
time.sleep(1)
i = 0
while(i < 11):
print(datetime.now())
i += 1
def many_thread():
threads = []
for _ in range(10): # 循环创建500个线程
t = threading.Thread(target=thread_func)
threads.append(t)
t.setDaemon(True) # 给每个子线程添加守护线程
for t in threads: # 循环启动500个线程
t.start()
for t in threads:
t.join() # 阻塞线程
if __name__ == '__main__':
many_thread()
print("thread end")

执行结果

程序会持续运行,不会立即打印“thread end”,因为主线程在调用join()后会阻塞,直到子线程执行结束。

疑问:有人可能认为这与不设置任何选项效果相同。实际上存在细微差别:若不设置任何选项,主线程会先执行完毕并打印“thread end”,随后等待子线程;若设置了join(),主线程会主动等待子线程结束,再继续执行后续代码。

对于死循环或长时间等待的情况,我们可以给join()设置超时参数。例如,将join的参数设为2秒,子线程会告知主线程最多等待2秒。若2秒内子线程结束,主线程继续执行;若2秒内未结束,主线程也会强制继续执行,随后关闭子线程。

输出结果


import threading
from datetime import datetime
import time
def thread_func(): # 线程函数
time.sleep(1)
i = 0
while(1):
print(datetime.now())
i += 1
def many_thread():
threads = []
for _ in range(10): # 循环创建500个线程
t = threading.Thread(target=thread_func)
threads.append(t)
t.setDaemon(True) # 给每个子线程添加守护线程
for t in threads: # 循环启动500个线程
t.start()
for t in threads:
t.join(2) # 设置子线程超时2秒
if __name__ == '__main__':
many_thread()
print("thread end")

运行程序后,约2秒时程序打印“thread end”并结束。这体现了阻塞线程的核心意义:通过控制主线程与子线程的执行顺序,实现更精细的流程控制。

总结

最后,再次明确守护线程与阻塞线程的定义:

  • 守护线程:子线程的生命周期依附于主线程,主线程结束,子线程无论是否执行完毕均被强制终止。
  • 阻塞线程:主线程通过join()方法等待子线程执行完毕,才继续执行后续代码或关闭程序。

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

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

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 算法库。