当前位置: 首页 > news >正文

python多线程操作,threading库详解(附实例演示)

目录

1 理解线程与进程

1.1进程(process)

1.2线程(thread)

1.3 主要区别

2 python的多线程

2.1 多线程的优点

3. python 的threading模块

3.2 python创建线程

3.2.1 继承thread类创建线程

3.2.2 直接使用Thread类创建线程

3.3 线程同步

3.3.1 使用local类实现线程同步

3.3.2 使用Event类实现线程通信

3.4 线程之间的通信

3.4.1 Queue各类方法使用方法及作用

3.4.2 使用Queue实现线程间的通信

结语:


1 理解线程与进程

经常能听到线程与进程这两个概念,这两个相似的概念容易被人混淆,为了讲清楚线程操作我们先讲清楚线程与进程的区别与相似点。

1.1进程(process)

一个进程是一个执行中的程序的实例。它包含程序代码和其当前活动的状态。每个进程都有自己的内存空间,包括代码段、堆栈段和数据段。在操作系统中,进程是资源分配和执行的基本单位。例如,在Windows系统中,一个运行的应用程序,如xx.exe,就是一个进程。

1.2线程(thread)

线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程在进程中并发执行,可以共享进程的资源,如内存和文件资源。每个线程有自己的程序计数器(PC)、虚拟机栈和本地方法栈,但是它们共享进程的堆和方法区资源。

1.3 主要区别

资源分配:进程是操作系统资源分配的基本单位,而线程是CPU调度和执行的单位。

内存空间:进程之间有独立的内存空间,线程则共享内存空间。

开销:创建和管理线程的开销小于进程,因为线程共享其所属进程的资源。

通信:线程间的通信更为方便,因为它们共享内存,而进程间通信(IPC)需要特定的机制,如管道、信号、消息队列等。

独立性:进程间相互独立,一个进程的崩溃不会影响其他进程,而一个线程的崩溃可能会影响整个进程。

通常来说一个进程至少包含一个线程,每一个进程可以拥有多个线程。同一进程内的线程共享同一片内存空间这极大地方便了线程之间的通信。线程是进程的执行路径,它们在进程的环境中并发执行。

2 python的多线程

在python官方文档中是这样形容多线程技术的:

线程是一种对于非顺序依赖的多个任务进行解耦的技术。多线程可以提高应用的响应效率,当接收用户输入的同时,保持其他任务在后台运行。一个有关的应用场景是,将 I/O 和计算运行在两个并行的线程中。

所以说当一个任务需要长时间占用cpu资源时我们就可以利用这一项解耦技术,将该任务放入线程中,以便其他任务正常进行。

我这里也给出官方文档地址11. 标准库简介 —— 第二部分 — Python 3.13.7 文档

2.1 多线程的优点

使用线程可以把占据长时间的程序中的任务放到后台去处理。

用户界面可以更加吸引人,比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。

程序的运行速度可能加快。

在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。

每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。

指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。

线程的特点:

  • 线程可以被抢占(中断)。
  • 在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) -- 这就是线程的退让。

线程可以分为:

  • 内核线程:由操作系统内核创建和撤销。
  • 用户线程:不需要内核支持而在用户程序中实现的线程。

3. python 的threading模块

在官方文档中之这样描述threading模块的:

threading 模块提供了一种在单个进程内部并发地运行多个 线程 (从进程分出的更小单位) 的方式。 它允许创建和管理线程,以便能够平行地执行多个任务,并共享内存空间。 线程特别适用于 I/O 密集型的任务,如文件操作或发送网络请求,在此类任务中大部分时间都会消耗于等待外部资源。这也表明我们要通过python创建和管理线程,需要掌握threading中的方法和属性。

这里我也给出python官方文档对该模块描述的地址:这个文档详细的讲述了threading库的使用方法。threading --- 基于线程的并行 — Python 3.13.7 文档

3.2 python创建线程

python中可以同过threading库中的thread类创建一个新的进程。下面将介绍两种方法创建线程,继承thread类和直接使用thread类。

创建线程我们用到了thread类中的几个常用方法,这是管理创建线程必要的方法。

__init__(self, group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None):方法

初始化Thread对象。

group:线程组,暂时未使用,保留为将来的扩展。

target:线程将要执行的目标函数。

name:线程的名称。

args:目标函数的参数,以元组形式传递。

kwargs:目标函数的关键字参数,以字典形式传递。

daemon:指定线程是否为守护线程。 

start(self):方法

启动线程。将调用线程的run()方法。

run(self):方法

线程在此方法中定义要执行的代码。

join(self,timeout=None):方法

等待线程终止。默认情况下,join()会一直阻塞,直到被调用线程终止。如果指定了timeout参数,则最多等待timeout秒。

3.2.1 继承thread类创建线程

这段代码创建了两个并行进程,两个任务同时进行展示了”多线程“效果:远行会产生下面的效果

Thread-1 和 Thread-2 会交替执行:你会看到它们的打印结果穿插出现(比如先 Thread-1 打印一次,然后 Thread-2 打印一次,或者反过来),每个线程都会执行 5 次打印(因为 counter=5),每次打印前会暂停 1 秒,等待两个子线程都完成后,打印 "Exiting Main Thread",可以看出cpu资源没有等待两个任务进行时互不干扰。

import threading
import timeclass MyThread(threading.Thread):def __init__(self, thread_id, name):threading.Thread.__init__(self)self.thread_id = thread_idself.name = namedef run(self):print(f"Starting {self.name}")# 线程要执行的任务print_time(self.name, 5)print(f"Exiting {self.name}")def print_time(thread_name, counter):for i in range(counter):time.sleep(1)print(f"{thread_name}: {time.ctime(time.time())}")# 创建线程
thread1 = MyThread(1, "Thread-1")
thread2 = MyThread(2, "Thread-2")# 启动线程
thread1.start()
thread2.start()# 等待线程完成
thread1.join()
thread2.join()print("Exiting Main Thread")
3.2.2 直接使用Thread类创建线程

效果和作用跟上述代码相同,区别在于直接使用target参数直接规定我们线程想要调用的函数

import threading
import timedef print_time(thread_name, delay, counter):while counter:time.sleep(delay)print(f"{thread_name}: {time.ctime(time.time())}")counter -= 1# 创建线程
thread1 = threading.Thread(target=print_time, args=("Thread-1", 1, 5))
thread2 = threading.Thread(target=print_time, args=("Thread-2", 2, 3))# 启动线程
thread1.start()
thread2.start()# 等待线程完成
thread1.join()
thread2.join()print("Exiting Main Thread")

3.3 线程同步

在threading库中有多种实现线程同步的方法和属性下面将选取几种较为常见的来使用讲解

3.3.1 使用local类实现线程同步

在官方文档中详细提到了Lock类的使用,还对不同版本的python中Lock类的变更做出了解释

下面给出Lock类中方法的使用:

实现原始锁对象的类。一旦一个线程获得一个锁,会阻塞随后尝试获得锁的线程,直到它被释放;任何线程都可以释放它。

acquire(blocking=Truetimeout=-1):方法

可以阻塞或非阻塞地获得锁。

当调用时参数 blocking 设置为 True (缺省值),阻塞直到锁被释放,然后将锁锁定并返回 True 。

在参数 blocking 被设置为 False 的情况下调用,将不会发生阻塞。如果调用时 blocking 设为 True 会阻塞,并立即返回 False ;否则,将锁锁定并返回 True

当参数 timeout 使用设置为正值的浮点数调用时,最多阻塞 timeout 指定的秒数,在此期间锁不能被获取。设置 timeout 参数为 -1 specifies an unbounded wait. It is forbidden to specify a timeout when blocking is False 。

如果成功获得锁,则返回 True,否则返回 False (例如发生 超时 的时候)。

release():方法

释放一个锁。这个方法可以在任何线程中调用,不单指获得锁的线程。

当锁被锁定,将它重置为未锁定,并返回。如果其他线程正在等待这个锁解锁而被阻塞,只允许其中一个允许。

当在未锁定的锁上唤起时,会引发 RuntimeError。

没有返回值。

locked()¶:方法

当锁被获取时,返回 True

示例代码:

下面代码创建了两个并行的线程,两个想成共享同一个变量,同一个锁。两个线程同时开始对一个变量进行+1操作,互斥锁保证每次有且只有一个线程对该变量进行+1操作,最后得到准确的数值。

import multiprocessingdef add_count(lock, count):for _ in range(100000):with lock:  # 自动获取和释放锁,确保同一时间只有一个进程执行count.value += 1  # count是共享变量(Value类型)if __name__ == "__main__":# 创建共享变量(Value需指定类型,'i'表示整数)count = multiprocessing.Value('i', 0)lock = multiprocessing.Lock()  # 创建锁# 创建2个进程p1 = multiprocessing.Process(target=add_count, args=(lock, count))p2 = multiprocessing.Process(target=add_count, args=(lock, count))p1.start()p2.start()p1.join()p2.join()print(f"最终结果:{count.value}")  # 正确输出200000(无锁时可能小于200000)
"""
当一个进程进入with lock:代码块时,会获取锁(此时其他进程会被阻塞,无法进入该代码块);
进程安全地执行count.value += 1(三步操作完整执行,不会被其他进程打断);
离开with代码块时,会自动释放锁,允许其他进程获取锁并执行操作。通过该简洁代码块代替方法acquire(blocking=True, timeout=-1):
和release():的结合使用
"""
3.3.2 使用Event类实现线程通信

话不多说还是先介绍一下event类中各个方法的使用方法和他们的在线程操作中的作用

1. set() 方法

  • 作用:将Event内部的标志位设置为True("已触发" 状态)。
  • 效果:所有因调用wait()方法而阻塞的进程会被立即唤醒,继续执行。
  • 使用场景:当一个进程完成某个关键操作(如初始化、数据准备)后,通过set()通知其他等待的进程可以继续工作。

2. clear() 方法

  • 作用:将Event内部的标志位重置为False("未触发" 状态)。
  • 效果:后续调用wait()的进程会再次进入阻塞状态(除非再次调用set())。
  • 使用场景:需要重复控制进程等待 / 唤醒时,用clear()重置标志位,让Event可以重复使用。

3. is_set() 方法

  • 作用:检查Event内部的标志位状态。
  • 返回值True(标志位已设置)或False(标志位未设置)。
  • 使用场景:进程需要根据标志位状态决定是否执行某些操作,而不是盲目等待(避免不必要的阻塞)。

4. wait(timeout=None) 方法

  • 作用:使当前进程阻塞,直到Event的标志位变为True,或超时(如果指定了timeout)。
  • 参数timeout(可选):超时时间(单位:秒)。如果为None(默认),则永久阻塞,直到标志位变为True
  • 返回值True(标志位变为True而被唤醒)或False(因超时而唤醒)。
  • 使用场景:进程需要等待某个条件满足(标志位为True)才能继续执行,且可以设置最大等待时间(避免无限阻塞)。

代码示例:

下面代码创建了两个并行的线程和一个共享的事件(event),实验现象也很简单明了,当事件A执行完后触发事件event.set(),然后事件B在执行完毕。

import multiprocessing
import timedef process_a(event):print("进程A开始执行")time.sleep(3)  # 模拟耗时任务print("进程A执行完毕,通知进程B")event.set()  # 触发事件def process_b(event):print("进程B等待进程A完成...")event.wait()  # 等待事件触发print("进程B开始执行")if __name__ == "__main__":event = multiprocessing.Event()p1 = multiprocessing.Process(target=process_a, args=(event,))p2 = multiprocessing.Process(target=process_b, args=(event,))p1.start()p2.start()p1.join()p2.join()

事件的同步到这里就结束了,其实线程的同步还有很多种方法可以实现比如信号量(Semaphore

3.4 线程之间的通信

在线程通信常使用queue.Queue。queue.Queue是线程安全的队列实现,专门用于线程间传递数据。其内部通过锁机制保证多线程操作时的数据一致性,无需额外加锁。

3.4.1 Queue各类方法使用方法及作用
方法作用关键参数说明
put(item, block=True, timeout=None)向队列添加元素block=True:队列满时阻塞等待,直到有空间
block=False:队列满时直接抛Queue.Full异常
timeout:超时时间(秒),超时后抛Queue.Full
get(block=True, timeout=None)从队列取出并删除元素(默认取队首元素)block=True:队列空时阻塞等待,直到有元素
block=False:队列空时直接抛Queue.Empty异常
timeout:超时时间,超时后抛Queue.Empty
qsize()返回队列中当前元素数量(多线程下可能不准确,仅作参考)-
empty()判断队列是否为空(可能有误差返回True表示为空,False表示非空(多线程中刚判断完可能立即被其他线程修改)
full()判断队列是否已满(可能有误差返回True表示已满,False表示未满
task_done()通知队列 “某个元素已被处理完毕”,配合join()使用每个get()取出的元素,需调用一次task_done(),告诉队列 “该任务已完成”
join()阻塞当前线程,直到队列中所有元素都被处理(即所有task_done()被调用)用于等待所有任务完成后再继续执行后续逻辑
3.4.2 使用Queue实现线程间的通信

下面代码采用经典的生产者—消费者模型展示了线程之间的安全通信。每当生产者线程放入一个数据消费者才能获取一个数据

from queue import Queue
import threading
import time# 生产者:向队列中添加数据
def producer(q, name):for i in range(3):item = f"数据{i}"q.put(item)  # 放入队列,队列满时会阻塞print(f"生产者{name}放入:{item},当前队列大小:{q.qsize()}")time.sleep(0.5)  # 模拟生成数据耗时# 消费者:从队列中取数据并处理
def consumer(q, name):while True:item = q.get()  # 取出数据,队列为空时会阻塞print(f"消费者{name}处理:{item}")time.sleep(1)  # 模拟处理耗时q.task_done()  # 通知队列该元素已处理完毕if __name__ == "__main__":q = Queue(maxsize=2)  # 队列最大容量为2# 创建生产者和消费者线程p1 = threading.Thread(target=producer, args=(q, "P1"))c1 = threading.Thread(target=consumer, args=(q, "C1"), daemon=True)  # 守护线程:主程序退出时自动结束p1.start()c1.start()p1.join()  # 等待生产者完成q.join()   # 等待队列中所有元素被处理完毕print("所有任务处理完成")

结语:

本文到这里就结束了,线程的管理是十分复杂的,当面对问题或是复杂多线程时还是建议看看官方文档寻求答案。

http://www.dtcms.com/a/348633.html

相关文章:

  • No static resource报错
  • Linux 系统管理核心概念与常用命令速查
  • Baumer高防护相机如何通过Tiny-YOLO单类模型实现人体跌倒检测与跟踪(C#代码UI界面版)
  • [Windows] PDF-XChange Editor Plus官方便携版
  • 鸿蒙中点击完成时延分析
  • 通过python程序将实时监测数据写入excel软件进行保存是常用和非常实用的功能,本文教会大家怎么去搞定此功能
  • LangChain框架入门19: 构建你的第一个 AI 智能体
  • HTTP报文格式详解:从历史演进到现代Web的通信基石
  • Python-鸭子类型
  • DBeaver连接SQL Server时添加驱动后仍提示找不到驱动的解决方法
  • 校园跑腿小程序源码 _ 跑腿便利店小程序 含搭建教程
  • 小程序全局状态管理:使用MobX进行跨组件数据共享详解(九)
  • c++基础知识入门
  • 【AI智能体】Dify 搭建业务单据差异核对助手实战详解
  • kubernetes中的认证和授权
  • Python 变量 (variables)、对象 (objects) 和引用 (references)
  • 第1章:量子涟漪
  • 双网卡并行访问:解决有线局域网与无线互联网共存时的路由冲突问题
  • 淘宝pc端首页做了哪些性能优化?
  • 大型 C/C++ 项目中 AI 助手(Cursor / Claude Code)日常操作清单与发散思路
  • MYSQL-表的约束(下)
  • 数据建模怎么做?一文讲清数据建模全流程
  • 设备管理与策略
  • UE5.5 C++ 增强输入 快速上手
  • nginx部署goaccess监控
  • JdbcTemplate和MyBatis的区别
  • 《支付回调状态异常的溯源与架构级修复》
  • 学习制作记录(选项UI以及存档系统)8.24
  • KVM虚拟化
  • Vue3 setup代替了vue2的哪些功能