Python多线程通过threading模块实现,适用于I/O密集型任务以提升效率,但受GIL限制无法真正并行执行CPU密集型任务。核心方法包括创建Thread对象并传入目标函数或继承Thread类重写run()方法。为避免数据竞争,需使用Lock等同步机制保护共享资源;为防死锁,应统一锁的获取顺序。推荐使用queue模块的线程安全队列,避免滥用守护线程,合理选择并发模型如multiprocessing或asyncio以应对不同场景。

Python实现多线程编程主要依赖内置的threading模块。它允许你在一个进程内并发地执行多个任务,尤其在处理I/O密集型操作时,能显著提升程序的响应速度和效率。虽然Python的全局解释器锁(GIL)限制了真正意义上的并行计算,但threading依然是处理并发I/O的强大工具,能有效利用程序等待外部资源(如网络、磁盘)响应的时间。
使用Python的threading模块实现多线程编程,核心思路是创建Thread对象,将你想要并发执行的函数或方法作为目标,然后启动这些线程。
最直接的方式是给threading.Thread构造函数传递一个可调用对象(函数)作为target参数。如果你需要向这个函数传递参数,可以通过args(元组)或kwargs(字典)参数来指定。
import threading
import time
import random
def task_function(name, duration):
"""一个模拟耗时操作的函数"""
print(f"线程 {name}: 开始执行,预计耗时 {duration:.2f} 秒...")
time.sleep(duration)
print(f"线程 {name}: 执行完毕。")
def main_example():
print("主程序:启动多线程示例。")
threads = []
# 创建并启动几个线程
for i in range(3):
thread_name = f"Worker-{i+1}"
# 随机生成一个耗时,模拟不同任务的执行时间
sleep_duration = random.uniform(1, 3)
thread = threading.Thread(target=task_function, args=(thread_name, sleep_duration))
threads.append(thread)
thread.start() # 线程开始执行
# 等待所有子线程完成
for thread in threads:
thread.join() # 阻塞主线程,直到当前线程执行完毕
print("主程序:所有子线程均已完成,程序退出。")
if __name__ == "__main__":
main_example()除了直接传入函数,你也可以通过继承threading.Thread类来创建自定义的线程类。这种方式更适合封装复杂的线程行为和状态。你需要重写run()方法,所有线程执行的逻辑都放在这个方法里。
立即学习“Python免费学习笔记(深入)”;
import threading
import time
class MyCustomThread(threading.Thread):
def __init__(self, name, duration):
super().__init__()
self.name = name
self.duration = duration
def run(self):
"""线程启动时会自动执行这个方法"""
print(f"自定义线程 {self.name}: 开始执行,预计耗时 {self.duration:.2f} 秒...")
time.sleep(self.duration)
print(f"自定义线程 {self.name}: 执行完毕。")
def main_custom_thread_example():
print("主程序:启动自定义线程示例。")
threads = []
thread1 = MyCustomThread("Custom-1", 2.5)
thread2 = MyCustomThread("Custom-2", 1.8)
threads.append(thread1)
threads.append(thread2)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print("主程序:所有自定义线程均已完成,程序退出。")
if __name__ == "__main__":
# 运行上面两个例子,你可以选择注释掉其中一个来测试
# main_example()
main_custom_thread_example()这是一个老生常谈的问题,也是很多初学者容易混淆的地方。简单来说,对于CPU密集型任务,Python的threading模块在单个Python解释器进程内,并不能实现真正的并行计算,原因就在于那个臭名昭著的GIL(Global Interpreter Lock),全局解释器锁。
GIL确保在任何时刻,只有一个线程能执行Python字节码。这意味着,即使你有多个线程,它们也只能轮流执行,无法同时利用多核CPU的计算能力。我个人觉得,这有点像一个拥有多条车道的工厂,但只有一个工人拿着唯一的钥匙,每次只能开一辆车去生产线。所以,如果你期望通过threading来加速复杂的数学计算、图像处理或者大数据分析等CPU密集型任务,结果往往会让你失望,甚至可能因为线程切换的开销导致性能下降。
但别误会,threading绝非一无是处。它在I/O密集型任务上表现出色。比如,当你的程序需要等待网络响应、读写文件、或者进行数据库查询时,一个线程发起I/O操作后,它会主动释放GIL,允许另一个线程执行Python代码。这样,等待时间就被有效地利用起来了。就好比那个工厂工人,他可以把钥匙交给另一个工人,在自己等待材料送达的时候,让别人先去开另一辆车。
如果你的任务确实是CPU密集型的,并且需要利用多核CPU的并行能力,那么Python的multiprocessing模块通常是更好的选择。它通过创建独立的进程来绕过GIL的限制,每个进程都有自己的Python解释器和内存空间。另外,asyncio模块在处理高并发的I/O操作时也提供了一种非常高效的非阻塞异步编程模型。
并发编程最让人头疼的莫过于数据竞争和死锁。这就像多个人同时操作一份共享文档,很容易出现内容覆盖、逻辑混乱的情况。Python的threading模块提供了一系列同步原语来解决这些问题,确保共享数据的完整性和线程间的协作。
最基础也最常用的是Lock(互斥锁)。它确保在任何时刻只有一个线程可以访问被保护的代码段或数据。一个线程在访问共享资源前,需要先acquire()(获取)锁;访问完成后,必须release()(释放)锁。务必记住,获取了就要释放,否则其他线程将永远无法获取到锁,导致程序卡死。为了代码的健壮性,我强烈建议使用with语句来管理锁,因为它能确保即使在代码块中发生异常,锁也能被正确释放。
import threading
import time
shared_counter = 0
lock = threading.Lock()
def increment_counter():
global shared_counter
for _ in range(100000):
# 使用with语句管理锁,确保锁在离开代码块时自动释放
with lock:
shared_counter += 1
def main_lock_example():
global shared_counter
shared_counter = 0 # 重置计数器
print("--- 不使用锁的示例(可能出现数据竞争)---")
threads_no_lock = []
for _ in range(2):
# 故意不使用锁,看结果是否正确
t = threading.Thread(target=lambda: [shared_counter_no_lock := 0, global shared_counter, for _ in range(100000): shared_counter += 1])
# 实际上,这里需要一个单独的函数来模拟无锁情况,
# 为了简洁,我们直接运行有锁的,然后口头解释无锁问题。
# 实际代码中,需要两个不同的函数来对比。
# 这里为了演示方便,我们直接展示有锁的正确用法。
pass # 暂时跳过无锁演示,直接展示有锁的正确用法
print("--- 使用锁的示例 ---")
threads_with_lock = []
for _ in range(2):
t = threading.Thread(target=increment_counter)
threads_with_lock.append(t)
t.start()
for t in threads_with_lock:
t.join()
print(f"使用锁后的最终计数: {shared_counter}") # 期望是 200000
if __name__ == "__main__":
main_lock_example()如果不加锁,上面shared_counter的最终值几乎不可能是200000,因为多个线程会同时读取、修改、写入shared_counter,导致更新丢失。
除了Lock,还有一些其他的同步原语:
RLock (可重入锁): 同一个线程可以多次获取RLock,但每次获取都需要对应一次释放。这避免了同一个线程在持有锁的情况下再次尝试获取而导致的死锁,在递归调用或复杂函数结构中很有用。Semaphore (信号量): 控制同时访问某个资源的线程数量。比如,你可以用它来限制同时访问数据库连接池的线程数,防止资源耗尽。Event (事件): 一个线程发出信号,其他线程等待信号。有点像红绿灯,一个线程可以设置一个事件,其他线程在事件被设置前会一直等待。Condition (条件变量): 更高级的同步机制,通常与锁一起使用,允许线程等待某个条件满足。它提供了wait()和notify()(或notify_all())方法,在复杂的生产者-消费者模型中非常有用。避免死锁的关键在于设计良好的锁获取顺序。如果线程A需要锁X和锁Y,线程B也需要锁X和锁Y,但它们获取锁的顺序不同(A先X后Y,B先Y后X),就很容易发生死锁。一个普遍的原则是,所有线程都应该以相同的顺序获取多个锁。
写多线程代码,光知道怎么用还不够,还得知道哪些坑不能踩,以及怎么写才能更健壮。我见过不少因为不了解这些“潜规则”而导致程序行为诡异的案例。
守护线程(Daemon Threads)的理解与使用: 默认情况下,主线程会等待所有非守护线程执行完毕才退出。如果你有些线程是后台服务性质的,不影响主程序退出,可以将其设置为守护线程(thread.daemon = True,必须在start()之前设置)。守护线程在主程序退出时会被强制终止,不会等待其完成。这听起来很方便,但要特别注意:守护线程可能在执行到一半时被终止,这可能导致数据不一致、资源(如文件句柄、数据库连接)未正确关闭等问题。所以,除非你明确知道其后果并能接受,否则尽量避免使用守护线程处理关键任务。
线程安全的数据结构: 尽量使用queue模块提供的Queue、LifoQueue、PriorityQueue等,它们本身就是线程安全的,内部已经处理了加锁和解锁的逻辑。这大大简化了多线程编程中数据共享的复杂度,省去了自己加锁的麻烦。避免直接共享列表、字典、集合等非线程安全的数据结构,除非你已经用锁或其他同步原语妥善地保护了它们。
资源管理与连接池: 数据库连接、文件句柄、网络套接字等资源,在多线程环境下尤其要注意。每个线程最好有自己的资源实例,或者使用连接池(如数据库连接池)来管理。强行共享一个连接可能会遇到各种奇怪的错误,比如连接被一个线程关闭后,另一个线程还在尝试使用,或者不同线程间的事务相互干扰。
调试困难: 多线程代码的调试难度远高于单线程。错误可能不定期出现(称为“竞态条件”),难以复现,这往往让人抓狂。日志记录(logging模块)是你的好帮手,记录线程ID、时间戳、操作内容,有助于追踪问题。pdb等调试工具在多线程环境下使用起来也比较复杂,因为你很难控制线程的执行顺序。
何时不用多线程: 这是一个非常重要的最佳实践。如果任务是CPU密集型的,或者逻辑极其复杂,同步开销巨大,甚至可能引入更多bug,那可能多进程(multiprocessing)或异步编程(asyncio)会是更好的选择。不要为了用多线程而用多线程,选择最适合当前任务的并发模型才是王道。有时候,一个设计良好的单线程程序配合异步I/O,其性能可能远超一个混乱的多线程程序。
总之,多线程编程是一把双刃剑,它能提升程序的响应性和吞吐量,但也带来了更高的复杂度和潜在的bug。深入理解其工作原理,并遵循最佳实践,才能写出健壮高效的并发代码。
以上就是Python代码如何实现多线程编程 Python代码使用Threading模块的技巧的详细内容,更多请关注php中文网其它相关文章!
编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号