Python并发编程:多线程

前序博客中已经介绍了基于多进程的并发编程,本篇主要介绍基于多线程的并发编程。

1 全局解释锁

1.1 定义

  全局解释锁(Global Interpreter Lock,简称GIL)是Python(特别是CPython)解释器中的一个机制,这个机制会限制同一时间只有一个线程执行Python字节码。GIL的好处主要有以下两点:

  • 保护解释器的全局状态:在多线程环境中,Python解释器内部的许多数据结构是全局共享的,GIL通过确保同一时刻只有一个线程执行字节码,防止了多个线程同时修改这些数据结构而导致的数据不一致或崩溃。
  • 简化内存管理:Python的内存管理系统需要在对象分配和释放时更新引用计数。GIL使得这一操作变得线程安全,而无需使用复杂的锁机制。

  但GIL也会带来一些负面影响。比如使Python多线程无法充分利用多核CPU的优势。所以本篇要介绍的Python多线程并没有实现真正的并行(Parallel)执行,而是并发(Concurrent)执行。
Tips: 并发是指在同一时间段内多个任务交替执行,可能是单核处理器通过时间片轮转实现,也可能是在多核处理器上通过操作系统的调度实现。虽然多个任务看起来是同时进行的,但实际上每个任务在某个时间点上并没有真正同时执行。并行是指在同一时刻多个任务真正同时执行。并行通常需要多核处理器或多个处理器核心,每个任务在不同的处理器核心上运行,从而实现真正的同时执行。

2 多线程

  Python中的threading模块可以实现多线程。本篇主要基于该模块介绍python中多线程的一般用法。

2.1 创建和启动线程

  Python中可以通过两种方法创建线程。一种是通过重写threading.Thread中的run方法;而是直接把目标函数传递给threading.Thread类。具体代码举例:

import threading
import time
import datetime# 方式1: 继承Thread类
class MyThread(threading.Thread):def run(self):print(f"Thread {self.name} is running:")time.sleep(2)print(f"Thread {self.name} is completed.")# 方式2: 直接传递目标函数
def thread_function(name):print(f"Thread {name} is running:")time.sleep(2)print(f"Thread {name} is completed.")# 创建和启动线程
threads = []
for i in range(3):t1 = MyThread(name=f"Thread-{i+1}")t2 = threading.Thread(target=thread_function, args=(f"Thread-{i+4}",))threads.append(t1)threads.append(t2)
print("Start all threads:",datetime.datetime.now())
# 启动所有线程
for t in threads:t.start()
# 等待所有线程完成
for t in threads:t.join()print("All threads completed:",datetime.datetime.now())

其代码执行结果如下:

Start all threads: 2024-05-24 17:18:28.677731
Thread Thread-1 is running:
Thread Thread-4 is running:
Thread Thread-2 is running:
Thread Thread-5 is running:
Thread Thread-3 is running:
Thread Thread-6 is running:
All threads completed: 2024-05-24 17:18:30.682527

从代码运行结果中可以看到:6个线程总共运行了2s,这似乎与前文说的“Python多线程只能并发无法真正并行执行”的结论违背。出现这种情况是因为Python线程在执行time.sleep时释放了GIL,使得其他线程可以继续执行。在这种情况下,所有6个线程几乎同时启动并进入休眠状态,而不是一个接一个地进行,所以总的执行时间将是最长的单个线程的执行时间,即2秒,而不是所有线程执行时间的总和。
将上述代码中的每个线程的任务改成CPU密集型任务再来看代码运行结果:

import threading
import datetimedef target_function():sum=0for i in range(10000):for j in range(10000):sum+=i*j# 创建和启动线程
threads = []
for i in range(3):t1 = threading.Thread(target=target_function)threads.append(t1)print("验证执行一次target_function的时间")
print("验证开始:",datetime.datetime.now())
target_function()
print("验证结束:",datetime.datetime.now())
print("3个线程执行开始:",datetime.datetime.now())
# 启动所有线程
for t in threads:t.start()
# 等待所有线程完成
for t in threads:t.join()print("3个线程执行结束:",datetime.datetime.now())

其执行结果如下:

验证执行一次target_function的时间
验证开始: 2024-05-24 18:14:40.580747
验证结束: 2024-05-24 18:14:46.259962
3个线程执行开始: 2024-05-24 18:14:46.260006
3个线程执行结束: 2024-05-24 18:15:02.237260

从运行结果中可以看到,单次target_function的执行时间不到6s,而3个线程总的执行时间大致16s,和3个线程顺序执行的时间差不多。

2.2 线程同步

  线程同步可以防止多个线程在同一时间访问或修改共享数据,从而导致数据错误或程序行为异常。Python提供了多种线程同步机制,常见的包括锁、条件变量、信号量和事件等。

2.2.1 锁

  在Python多线程编程中,锁可以用于同步对多种共享资源的访问,确保线程安全和数据一致性。这些资源包括全局变量和实例变量、数据结构(如列表、字典等)、文件和I/O资源、数据库连接、线程之间的通信资源以及网络资源。目前threading模块中有两种锁类:

  • Lock:Lock对象是最基本的同步原语,有两种状态:锁定和非锁定。线程可以通过acquire()方法获得锁,如果锁已经被其他线程获得,则调用线程会被阻塞,直到锁被释放。线程通过release()方法释放锁,允许其他线程获得锁。
  • RLock(可重入锁):RLock对象允许同一个线程多次获得同一个锁而不会引起死锁。每次调用acquire()方法都会增加锁的计数,直到调用相应次数的release()方法,锁才会被释放。

具体代码举例如下:

import threading
import queuecounter = 0
def increment_counter():#累加器global counterfor _ in range(1000):with counter_lock: # 使用with语句简化锁的获取和释放counter += 1def producer():#生产者线程for i in range(10):#手动获取或释放锁queue_lock.acquire()shared_queue.put(i)queue_lock.release()def consumer():#消费者线程while True:with queue_lock:if not shared_queue.empty():item = shared_queue.get()else:breakclass SharedResource:def __init__(self):self.value = 0def increment(self):with rlock:self.value += 1self.double()def double(self):with rlock:self.value *= 2
def task():for _ in range(10):resource.increment()
if __name__=="__main__":#1.全局变量锁counter_lock = threading.Lock()threads = [threading.Thread(target=increment_counter) for _ in range(10)]for thread in threads:thread.start()for thread in threads:thread.join()print("全局变量的最终值:",counter)#2.共享队列锁shared_queue = queue.Queue()queue_lock = threading.Lock()producer_thread = threading.Thread(target=producer)consumer_thread = threading.Thread(target=consumer)producer_thread.start()producer_thread.join()consumer_thread.start()consumer_thread.join()#3.可重入锁rlock = threading.RLock()resource = SharedResource()threads = [threading.Thread(target=task, name=f'Thread-{i+1}') for i in range(3)]for thread in threads:thread.start()for thread in threads:thread.join()print(f"Final value: {resource.value}")

其代码运行结果如下:

全局变量的最终值: 10000
Final value: 2147483646
2.2.2 信号量

  信号量(Semaphore)是用于控制资源访问数量的同步原语。信号量内部有一个计数器,每次调用acquire()方法时计数器减1,每次调用release()方法时计数器加1。如果计数器为0,调用acquire()的线程将被阻塞,直到其他线程调用release()。其用法举例如下

import threading
import time
import datetime
semaphore = threading.Semaphore(2)  # 最多允许两个线程同时执行
def task(name):semaphore.acquire()print(f"Thread {name} starting:",datetime.datetime.now())time.sleep(5)print(f"Thread {name} finished:",datetime.datetime.now())semaphore.release()threads = [threading.Thread(target=task, args=(f'Thread-{i+1}',)) for i in range(5)]
for thread in threads:thread.start()
for thread in threads:thread.join()
print("All tasks completed")

其执行结果如下:

Thread Thread-1 starting: 2024-05-25 16:23:12.605692
Thread Thread-2 starting: 2024-05-25 16:23:12.605832
Thread Thread-1 finished: 2024-05-25 16:23:17.608979
Thread Thread-2 finished: 2024-05-25 16:23:17.608901
Thread Thread-3 starting: 2024-05-25 16:23:17.609524
Thread Thread-4 starting: 2024-05-25 16:23:17.609970
Thread Thread-3 finished: 2024-05-25 16:23:22.613643
Thread Thread-4 finished: 2024-05-25 16:23:22.613916
Thread Thread-5 starting: 2024-05-25 16:23:22.615044
Thread Thread-5 finished: 2024-05-25 16:23:27.618141
All tasks completed

从代码运行结果可以看到,限定资源数量为2之后,线程3、4是等到线程1、2结束才开始执行的。

2.3 线程通信

  线程通信指的是线程之间传递信息和协调行为的机制。线程通信可以通过多种方式实现,包括队列、事件、条件变量、信号量等。这里仅介绍事件和的条件变量。

2.3.2 事件

事件(Event)是一种用于线程间通信和同步的机制。事件对象允许一个或多个线程等待某个事件的发生,并在事件发生时被唤醒。threading类中的Event类主要包括以下几个方法:

  • set():将内部标志设置为True,并唤醒所有等待的线程。
  • clear():将内部标志设置为False。
  • wait(timeout=None):如果内部标志为True,则立即返回;否则阻塞线程,直到内部标志被设置为True或超时。
  • is_set():返回内部标志的当前状态。

其用法举例如下:

import threading
import timedef waiter(event):print("Waiter: Waiting for the event to be set...")event.wait()  # 等待事件被设置为Trueprint("Waiter: Event has been set, proceeding...")def setter(event):time.sleep(3)  # 模拟一些工作print("Setter: Setting the event")event.set()  # 将事件设置为True# 创建事件对象
event = threading.Event()# 创建并启动线程
waiter_thread = threading.Thread(target=waiter, args=(event,))
setter_thread = threading.Thread(target=setter, args=(event,))waiter_thread.start()
setter_thread.start()waiter_thread.join()
setter_thread.join()

其执行结果如下:

Waiter: Waiting for the event to be set...
Setter: Setting the event
Waiter: Event has been set, proceeding...
2.3.2 条件变量

  条件变量(Condition)允许一个线程等待特定条件的发生,并在条件满足时通知其他线程。它通常与锁一起使用。threading类中的Condition类一般包括以下方法:

  • wait():当前线程等待,直到其他线程调用notify()或notify_all()方法唤醒它。
  • notify():唤醒一个等待中的线程。
  • notify_all():唤醒所有等待中的线程。

其具体用法举例如下:

import threading
import time
import randomcondition = threading.Condition()
queue = []class Producer(threading.Thread):def run(self):global queuewhile True:item = random.randint(1, 10)with condition:queue.append(item)print(f"Produced {item}")condition.notify()  # 唤醒消费者线程time.sleep(random.random())class Consumer(threading.Thread):def run(self):global queuewhile True:with condition:while not queue:condition.wait()  # 等待生产者线程的通知item = queue.pop(0)print(f"Consumed {item}")time.sleep(random.random())# 创建并启动线程
producer = Producer()
consumer = Consumer()producer.start()
consumer.start()producer.join()
consumer.join()

补充

补充1: 释放GIL的操作

目前,Python中会导致线程释放GIL的操作主要包括以下几种:

  • I/O操作,例如文件读写、网络请求等。
  • 某些内置函数和标准库函数,例如 time.sleep()、select.select()、socket 模块中的操作。
  • 使用C语言编写的扩展模块可以显式地释放和重新获取GIL。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/840836.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

啥?题目是认真的么?啥大模型开源还是闭源,这就是个驳论!

开源大模型与闭源大模型,你更看好哪一方? 简介:评价一个AI模型“好不好”“有没有发展”,首先就躲不掉“开源”和“闭源”两条发展路径。对于这两条路径,你更看好哪一种呢? 其实我认为2者之间压根没啥好纠…

Java队列简介

在现代应用程序开发中,队列是一种常见且强大的数据结构,用于存储和管理待处理的任务序列。结合MySQL数据库,我们可以利用队列实现任务的持久化存储与高效处理。本文将通过四个案例,详细介绍如何在Java中使用队列,并结合…

面了字节大模型算法岗,太难了。。。

节前,我们组织了一场算法岗技术&面试讨论会,邀请了一些互联网大厂朋友、今年参加社招和校招面试的同学。 针对大模型技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何准备面试攻略、面试常考点等热门话题进行了深入的讨论。 汇总合集…

PCM和QAM

PCM(脉冲编码调制)和QAM(正交振幅调制)是两种不同的信号调制技术,它们在通信系统中有着不同的应用和特点。 PCM(脉冲编码调制) 概述 PCM是一种数字信号处理技术,用于将模拟信号转…

ClickHouse实战处理(一):MergeTree表引擎

MergeTree作为家族系列最基础的表引擎,主要有以下特点: 存储的数据按照主键排序:创建稀疏索引加快数据查询速度。支持数据分区,可以通过PARTITION BY语句指定分区字段。支持数据副本。支持数据采样。 一、MergeTree分类和建表参…

[emailprotected](2)核心概念-JSX

目录 1,什么是 jsx2,空标签3,通过大括号使用 js4,防止注入攻击5,元素的不可变性 官方文档 1,什么是 jsx Facebook 起草的 js 扩展语法。本质上是 js 对象,会被 babel 编译,最终转换…

军队仓库管理系统|DW-S301系统特点

部队仓库管理系统DW-S301系统通过数据采集、互联网和物联网技术,实现数字化智能管控,以提高军用物资的仓储准确率和流转率,缩短周转时间,降低库存成本,也有助于消除生产过程中的不确定性。 系统功能:通过部…

web学习笔记(五十六)

目录 1.绑定类名和style 1.1 绑定类名 1.1.1 绑定单个类名 1.1.2 绑定多个类名 1.2 style相关知识 2. vue的响应式原理 3. v-once 4.本地搭建Vue单页应用 4.1 安装Vue脚手架 4.2 安装对应的包文件 4.3 运行项目 1.绑定类名和style 1.1 绑定类名 1.1.1 绑定单个类名…

openstack报错:openstack volume service list The server is currently unavailable.

openstack报错&#xff1a; [rootcontroller ~]# openstack volume service list The server is currently unavailable. Please try again at a later time.<br /><br /> The Keystone service is temporarily unavailable.(HTTP 503)操作&#xff1a;做cinder 报…

计算机网络协议

网络协议 基于TCP的应用层协议 POP3&#xff08;Post Office Protocol 3&#xff09;&#xff1a; 用于支持客户端远程管理服务器上的电子邮件。它支持**“离线”邮件处理**&#xff0c;即邮件发送到服务器上后&#xff0c;一旦邮件被POP3客户端下载到本地计算机&#xff0c;…

NL6621 实现获取天气情况

一、主要完成的工作 1、建立TASK INT32 main(VOID) {/* system Init */SystemInit();OSTaskCreate(TestAppMain, NULL, &sAppStartTaskStack[NST_APP_START_TASK_STK_SIZE -1], NST_APP_TASK_START_PRIO); OSStart();return 1; } 2、application test task VOID TestAp…

【vue3】计算属性 computed 与 lazy

在 Vue 3 中&#xff0c;computed 和 lazy 都是与响应式系统和数据计算相关的概念&#xff0c;但它们具有不同的用途和行为。 computed 计算属性 computed 是 Vue 中的一个核心功能&#xff0c;用于声明依赖于其他响应式数据的计算属性。当计算属性的依赖数据发生变化时&…

Python3.10用Pyinstaller打包exe报错: IndexError: tuple index out of range

使用Python3.10时&#xff0c;Pyinstaller出现IndexError: tuple index out of range错误&#xff0c;部分异常信息如下&#xff1a; .......yield from get_instructions(code_object)File "C:\Program Files\Python\lib\dis.py", line 338, in _get_instructions_…

网页上的超链接复制到Excel中+提取出网址+如何保存

定义 超链接网页标题地址栏 使用的工具 2024年的WPS是不行的&#xff0c; 如果把知乎网页上的超链接复制到WPS中的Excel中&#xff0c;就会丢掉地址&#xff0c;只剩下网页标题 具体操作&#xff08;转载,在Excel2013上验证可行&#xff09; [1]启用【开发工具】&#xff…

Golang net/http标准库常用方法(三)

大家好&#xff0c;针对Go语言 net/http 标准库&#xff0c;将梳理的相关知识点分享给大家~~ 围绕 net/http 标准库相关知识点还有许多章节&#xff0c;请大家多多关注。 文章中代码案例只有关键片段&#xff0c;完整代码请查看github仓库&#xff1a;https://github.com/hltfa…

科技前沿:IDEA插件Translation v3.6 带来革命性更新,翻译和发音更智能!

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

python获取安装路径盘符

文章目录 一、前言二、实现方法一、前言 python写的客户端工具需要安装时,可以给用户一个默认的安装路径,如果直接写死个D、E、F盘什么的,那用户可能没有那个盘符,但是如果直接指定系统盘C盘,又不是那么友好,所以默认指定的安装路径应该尽量满足下面的要求: 盘符存在盘…

【MySQL精通之路】系统变量-系统变量权限

系统变量可以具有影响整个服务器操作的全局值&#xff0c;也可以具有仅影响当前会话的会话值&#xff0c;或者两者兼有&#xff1a; 对于动态系统变量&#xff0c;SET语句可用于更改其全局或会话运行时值&#xff08;或同时更改两者&#xff09;&#xff0c;以影响当前服务器实…

spring boot 启动类加载的优化

1减少依赖&#xff0c;不用的依赖去掉&#xff0c; 2、使用自定义配置bean进行加载&#xff0c;减少更多的不必要加载的组件 3、启动spring boot的配置懒加载模式&#xff0c; 4、将需要启动的bean写入到项目中的文件&#xff0c;去加载该目录下的bean文件 5、使用编译时优化&a…

利用天气API接口自己DIY一个预报小管家

天气预报查询API 是一种实用的日常工具&#xff0c;它通过编程方式为开发者提供实时的天气数据。开发者可以通过简单的代码调用&#xff0c;与天气预报服务提供商进行交互&#xff0c;获取特定地区的天气信息&#xff0c;如温度、湿度、风速、风向、降水量等&#xff0c;以及未…