python中如何实现多线程编程_Python threading模块多线程编程入门

冰火之心
发布: 2025-09-15 21:04:01
原创
918人浏览过
Python多线程通过threading模块实现,适用于I/O密集型任务,因GIL限制无法在CPU密集型任务中并行执行;此时应使用多进程。

python中如何实现多线程编程_python threading模块多线程编程入门

Python中实现多线程编程,主要依赖于其标准库中的

threading
登录后复制
模块。这个模块提供了一种高级、面向对象的API来创建和管理线程,让你可以将程序中的某些任务并发执行,尤其在处理I/O密集型操作时,能够显著提高程序的响应速度和效率。然而,对于CPU密集型任务,由于Python全局解释器锁(GIL)的存在,多线程并不能带来真正的并行计算优势,此时通常会考虑使用多进程。

解决方案

Python的

threading
登录后复制
模块是实现多线程的核心。最直接的方式是定义一个函数作为线程的执行体,然后创建
threading.Thread
登录后复制
对象,将这个函数作为
target
登录后复制
参数传入,最后调用
start()
登录后复制
方法启动线程。

一个基本的例子是这样的:

立即学习Python免费学习笔记(深入)”;

import threading
import time

def task(name, duration):
    """一个模拟耗时操作的函数"""
    print(f"线程 {name}: 正在启动...")
    time.sleep(duration) # 模拟I/O操作或计算
    print(f"线程 {name}: 完成。")

# 创建并启动线程
thread1 = threading.Thread(target=task, args=("Worker 1", 3))
thread2 = threading.Thread(target=task, args=("Worker 2", 2))

thread1.start() # 启动线程1
thread2.start() # 启动线程2

# 等待所有线程完成
thread1.join()
thread2.join()

print("所有线程都已执行完毕。")
登录后复制

在这个例子中,

task
登录后复制
函数是线程要执行的工作。我们创建了两个
Thread
登录后复制
实例,分别传入不同的参数。
start()
登录后复制
方法会启动线程,使其在后台运行。而
join()
登录后复制
方法则非常重要,它会阻塞主线程,直到对应的子线程执行完毕。如果没有
join()
登录后复制
,主线程可能在子线程完成前就退出了。

除了直接传入函数,更面向对象的方法是继承

threading.Thread
登录后复制
类,并重写其
run()
登录后复制
方法。这在需要线程维护自身状态或有更复杂生命周期管理时非常有用。

import threading
import time

class MyThread(threading.Thread):
    def __init__(self, name, duration):
        super().__init__()
        self.name = name
        self.duration = duration

    def run(self):
        print(f"线程 {self.name}: 正在启动...")
        time.sleep(self.duration)
        print(f"线程 {self.name}: 完成。")

thread3 = MyThread("Custom Thread 1", 4)
thread4 = MyThread("Custom Thread 2", 1)

thread3.start()
thread4.start()

thread3.join()
thread4.join()

print("自定义线程也已执行完毕。")
登录后复制

这种方式提供了更好的封装性,让线程的逻辑和数据可以更紧密地结合在一起。

Python多线程真的能提高程序性能吗?它与多进程有何不同?

这是一个我经常被问到的问题,也是很多初学者容易混淆的地方。简而言之,Python多线程能否提高性能,取决于你的任务类型。

对于I/O密集型任务(比如网络请求、文件读写、数据库查询),Python多线程确实能有效提高程序的响应速度和效率。这是因为当一个线程在等待I/O操作完成时(比如等待网络响应),Python的全局解释器锁(GIL)会被释放,允许其他线程运行。这样,CPU就可以在等待I/O的同时,处理其他线程的任务,从而提高整体的并发性。你可以想象成:打电话时,你不需要一直拿着听筒等着对方说话,可以把电话放在一边,做点别的事情,听到响声再拿起。

然而,对于CPU密集型任务(比如大量的数值计算、图像处理),Python多线程在标准的CPython解释器下,并不能实现真正的并行计算,也因此无法提高性能。原因就是那个臭名昭著的全局解释器锁(GIL)。GIL确保了在任何给定时刻,只有一个线程能够执行Python字节码。这意味着即使你有多个线程,它们也只能轮流获得GIL并执行代码,无法同时利用多核CPU的优势。在这种情况下,多线程反而可能因为上下文切换的开销,导致性能略有下降。

与多进程(

multiprocessing
登录后复制
模块)的区别

  • GIL的影响:多线程受GIL限制,无法在CPU密集型任务中实现并行。多进程则不受GIL限制,因为每个进程都有自己独立的Python解释器和GIL,它们可以在不同的CPU核心上真正并行运行。
  • 内存共享:线程在同一个进程中,共享相同的内存空间。这使得线程间数据共享变得容易,但也带来了数据竞争和同步的复杂性。进程之间则拥有独立的内存空间,数据共享需要通过特定的IPC(Inter-Process Communication)机制(如队列、管道、共享内存)来完成,这通常更复杂一些,但也更安全,因为避免了直接的内存冲突。
  • 创建开销:创建线程的开销通常比创建进程小。进程的创建需要复制父进程的内存空间(或使用写时复制技术),这会消耗更多资源和时间。
  • 适用场景:多线程适用于I/O密集型任务;多进程适用于CPU密集型任务。

所以,在选择多线程还是多进程时,首先要分析你的程序瓶颈在哪里。如果瓶颈在等待外部资源(网络、磁盘),那么

threading
登录后复制
很可能是个好选择;如果瓶颈在CPU计算,那么
multiprocessing
登录后复制
会是更合适的方案。

如何避免多线程中的数据竞争问题?锁(Lock)的实际应用

在多线程编程中,当多个线程尝试同时访问和修改同一个共享资源(比如一个全局变量、一个列表或一个文件)时,就可能发生数据竞争(Race Condition)。这会导致数据不一致、结果错误,甚至程序崩溃。这就像多个人同时往一个银行账户里存钱取钱,如果没有排队或协调,账目就容易乱掉。

为了解决这个问题,我们需要引入线程同步机制。Python的

threading
登录后复制
模块提供了多种同步原语,其中最基础和常用的是锁(Lock)

豆包AI编程
豆包AI编程

豆包推出的AI编程助手

豆包AI编程 483
查看详情 豆包AI编程

Lock
登录后复制
对象就像一个门卫,它只有一个钥匙。当一个线程想要访问共享资源时,它必须先尝试“获取”这把钥匙(即调用
acquire()
登录后复制
方法)。如果钥匙已经被其他线程拿走了,当前线程就会被阻塞,直到钥匙被释放。当线程完成对共享资源的访问后,它必须“释放”这把钥匙(即调用
release()
登录后复制
方法),以便其他等待的线程可以获取它。

来看一个经典的例子:多个线程对同一个计数器进行递增操作。

import threading
import time

counter = 0
# 创建一个锁对象
lock = threading.Lock()

def increment_counter():
    global counter
    for _ in range(100000):
        # 获取锁
        lock.acquire()
        try:
            counter += 1
        finally:
            # 确保锁被释放,即使发生异常
            lock.release()

threads = []
for i in range(5):
    thread = threading.Thread(target=increment_counter)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(f"最终计数器值: {counter}")
# 理论上应该是 5 * 100000 = 500000
登录后复制

如果没有锁的保护,

counter
登录后复制
的最终值几乎不可能是500000。因为
counter += 1
登录后复制
这个操作实际上包含了读取、递增、写入三个步骤,在这些步骤之间,线程可能会被切换,导致数据丢失

使用

lock.acquire()
登录后复制
lock.release()
登录后复制
是基础用法。更推荐的做法是使用
with
登录后复制
语句,因为它能确保锁在代码块执行完毕后(无论是否发生异常)自动释放,避免了忘记释放锁导致死锁的风险:

# ... (前面的导入和counter定义不变)

def increment_counter_with_lock():
    global counter
    for _ in range(100000):
        with lock: # 自动获取和释放锁
            counter += 1

# ... (创建和启动线程的代码不变)
登录后复制

这种

with lock:
登录后复制
的写法简洁又安全,是Python中处理锁的惯用模式。

除了

Lock
登录后复制
threading
登录后复制
模块还提供了其他更复杂的同步原语,比如:

  • RLock
    登录后复制
    (可重入锁)
    :同一个线程可以多次获取同一个
    RLock
    登录后复制
    ,但必须释放相同次数。适用于递归调用或同一个线程需要多次加锁的场景。
  • Semaphore
    登录后复制
    (信号量)
    :控制对共享资源的访问数量,而不是像
    Lock
    登录后复制
    那样只允许一个。可以限制同时访问某个资源的线程数量。
  • Event
    登录后复制
    (事件)
    :用于线程间的通信,一个线程发送事件,其他线程等待事件发生。
  • Condition
    登录后复制
    (条件变量)
    :比
    Event
    登录后复制
    更高级的通信机制,允许线程在特定条件满足时等待和被唤醒。

对于大多数数据竞争问题,

Lock
登录后复制
已经足够。选择合适的同步机制,是编写健壮多线程程序的关键。

守护线程(Daemon Thread)在Python多线程中扮演什么角色?

在Python的多线程世界里,线程可以分为两种:用户线程(或非守护线程)守护线程(Daemon Thread)。它们的区别在于,当主程序退出时,它们是否会阻止程序终止。

  • 用户线程:默认情况下,你创建的所有线程都是用户线程。这意味着,即使主线程执行完毕,只要有任何一个用户线程还在运行,Python解释器就不会退出,它会等待所有用户线程完成。
  • 守护线程:守护线程则不同。它是一种“后台”线程,当主线程退出时,无论守护线程是否完成,Python解释器都会强制终止整个程序,不会等待守护线程。你可以把守护线程想象成主程序的服务员,主程序一打烊,服务员就得立刻走人,不管手头的工作有没有做完。

如何设置守护线程?

在创建

Thread
登录后复制
对象之后,但在调用
start()
登录后复制
方法之前,将
daemon
登录后复制
属性设置为
True
登录后复制
即可:

import threading
import time

def daemon_task():
    print("守护线程: 正在启动,将运行5秒...")
    time.sleep(5)
    print("守护线程: 理论上会完成,但可能被提前终止。")

def normal_task():
    print("普通线程: 正在启动,将运行2秒...")
    time.sleep(2)
    print("普通线程: 完成。")

daemon_t = threading.Thread(target=daemon_task)
daemon_t.daemon = True # 设置为守护线程

normal_t = threading.Thread(target=normal_task)

daemon_t.start()
normal_t.start()

print("主线程: 启动了子线程,现在等待普通线程完成。")
normal_t.join() # 主线程等待普通线程完成

print("主线程: 普通线程已完成,现在主线程将退出。")
# 此时,如果daemon_t还没完成,它会被强制终止。
登录后复制

运行这段代码,你会发现

daemon_task
登录后复制
中的“理论上会完成”那句话可能不会被打印出来,因为它在主线程退出时被“斩断”了。

守护线程的适用场景:

守护线程通常用于那些不需要程序等待其完成的任务,例如:

  • 后台日志记录:持续将程序运行信息写入日志文件,即使主程序突然崩溃,也不影响主程序的退出。
  • 心跳包发送:定期向服务器发送心跳包以维持连接,主程序退出时,连接自然断开即可。
  • 缓存清理:定期清理过期缓存。
  • 资源监控:在后台监控系统资源使用情况。

使用守护线程的注意事项:

由于守护线程在主程序退出时会被突然终止,它们可能没有机会进行清理工作,比如关闭文件句柄、释放网络连接等。这可能导致资源泄露或数据损坏。因此,在使用守护线程时,要特别注意其任务的性质,确保即使被突然终止也不会造成严重后果。如果需要确保清理工作完成,那么它就不适合作为守护线程,或者需要额外的机制来优雅地关闭它。

以上就是python中如何实现多线程编程_Python threading模块多线程编程入门的详细内容,更多请关注php中文网其它相关文章!

python速学教程(入门到精通)
python速学教程(入门到精通)

python怎么学习?python怎么入门?python在哪学?python怎么学才快?不用担心,这里为大家提供了python速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号