Python小白学习教程从入门到入坑------第三十三课 线程(语法进阶)

一、多任务

在Python中,多任务处理指的是同时执行多个任务,以提高程序的效率和响应能力

多任务处理在需要处理大量I/O操作(如网络请求、文件读写等)或同时执行多个独立任务时特别有用

Python提供了几种实现多任务处理的方法,主要包括多线程、多进程和异步编程

二、多线程

2.1 进程和线程

进程:进程是操作系统进行资源分配和调度的基本单位。当我们打开一个程序时,操作系统会为该程序创建一个进程。这个进程会拥有独立的内存空间、文件句柄和其他系统资源。进程之间通过进程间通信(IPC)机制进行数据交换,以确保它们的独立性。

线程:线程是CPU调度的基本单位。每个进程在创建时都会默认拥有一个线程,这个线程被称为主线程。主线程负责执行进程的代码,并管理进程的生命周期。除了主线程外,进程还可以创建多个额外的线程来并发执行任务。

进程与线程的关系:

1、包含关系:进程包含线程,线程是进程的一部分。每个进程至少有一个线程(主线程),而线程则必须依附于某个进程才能存在和运行

2、资源共享:进程拥有独立的资源空间,而线程共享进程的资源。这使得线程之间的通信和数据交换更加高效,但也需要注意线程同步问题

3、并发执行:进程和线程都可以并发执行。操作系统通过调度线程来实现并发执行,而线程则依赖于进程的存在和运行环境

4、生命周期:进程的生命周期包括创建、运行、阻塞和终止等阶段。线程的生命周期则包括创建、就绪、运行、阻塞和终止等阶段。线程的生命周期受进程生命周期的影响,当进程终止时,其所有线程也会随之终止

2.2 多线程

在Python中,多线程指的是在单个进程中同时运行多个线程的能力。每个线程都是进程中的一个执行单元,它们共享进程的资源(如内存和文件句柄),但拥有自己独立的栈和程序计数器。多线程允许程序并发地执行多个任务,从而提高程序的效率和响应能力。

Thread类参数

1、target:这是一个必需的参数,它指定了要由线程运行的目标函数(也称为任务函数)。当线程启动时,它将调用这个函数

2、args:这是一个可选参数,它提供了一个元组,该元组包含了要传递给目标函数的参数。如果目标函数不需要参数,则可以省略此参数或将其设置为空元组( )

3、kwargs:这也是一个可选参数,它提供了一个字典,该字典包含了要传递给目标函数的关键字参数。如果目标函数不需要关键字参数,则可以省略此参数或将其设置为空字典{ }

eg:使用threading.Thread类来创建一个线程,并传递参数给目标函数

# 导入线程模块
import threading
# 导入time模块,用于模拟耗时操作
import time


def sing(name):
    print(f"{name}在唱歌")
    time.sleep(2)  # 使程序暂停执行2秒,模拟唱歌的耗时
    print("唱完歌了")


def dance(name2):
    print(f"{name2}在跳舞")
    time.sleep(2)  # 使程序暂停执行2秒,模拟跳舞的耗时
    print("跳完舞了")


# 主程序入口
if __name__ == "__main__":
    # 1. 创建子线程
    # 创建两个线程对象t1和t2,它们分别执行sing()和dance()函数
    t1 = threading.Thread(target=sing,args=('junjun',))   # 已元组的形式传参,()后面记得用逗号隔开
    t2 = threading.Thread(target=dance,args=('junjun',))

    # 2. 开启子线程
    # 通过调用start()方法,线程t1和t2开始执行它们的目标函数
    t1.start()
    t2.start()

    # 3. 阻塞主线程,等待子线程结束
    # join()方法会阻塞调用它的线程(在这里是主线程),直到被调用的线程(t1和t2)执行结束
    # 这意味着主线程会等待t1和t2都执行完毕后,才会继续执行后面的代码
    t1.join()
    t2.join()

    # 4. 获取线程名
    print(t1.getName())
    print(t2.getName())
    # 5. 更改线程名
    t1.setName("子线程一")
    t2.setName("子线程二")
    # 当t1和t2都执行完毕后,主线程继续执行,打印这条消息
    print("完美谢幕,本次表演结束")

由于线程是并发执行的,因此输出的顺序可能会有所不同,但大致上应该是这样的(假设唱歌的线程先执行): 

junjun在唱歌
junjun在跳舞
唱完歌了跳完舞了

Thread-1
Thread-2
完美谢幕,本次表演结束

2.3 多线程的三个特点

1、线程之间执行是无序的

2、线程之间共享资源

3、资源竞争

2.3.1 线程之间的执行是无序的

线程执行是根据cpu调度决定的,多线程程序的一个关键特性是线程之间的执行顺序是不确定的。这意味着,虽然您可以启动多个线程并按照某种顺序编写它们的代码,但操作系统调度这些线程的方式决定了它们实际上将如何交替执行。

因此,在不同的运行或相同的运行但不同的时间点,线程的执行顺序可能会有所不同。这种无序性使得多线程程序难以调试和预测其行为。

eg:使用threading模块来演示线程之间的执行无序性

import threading
import time

# 定义线程要执行的函数
def thread_function(name, duration):
    print(f"Starting {name}")
    time.sleep(duration)  # 模拟线程正在做一些工作
    print(f"Ending {name}")

# 创建线程
thread1 = threading.Thread(target=thread_function, args=("Thread-1", 2))
thread2 = threading.Thread(target=thread_function, args=("Thread-2", 1))

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

# 等待线程完成(虽然在这个例子中不是必需的,因为我们没有主线程需要等待它们的结果)
thread1.join()
thread2.join()

print("All threads have finished.")

在这个例子中,我们创建了两个线程thread1和thread2,它们分别执行thread_function函数,该函数接受一个线程名称和一个持续时间作为参数。time.sleep(duration)用于模拟线程正在执行一些耗时操作。

尽管我们按照thread1.start()和thread2.start()的顺序启动了线程,但thread2可能会因为持续时间较短而先于thread1结束

由于线程调度的不确定性,每次运行这个程序时,线程的输出顺序都可能不同

可能的输出之一(但不是唯一的):

Starting Thread-1
Starting Thread-2
Ending Thread-2
Ending Thread-1
All threads have finished.

但也有可能Thread-2先开始并结束,或者两个线程的输出几乎同时出现(尽管由于time.sleep的存在,这种情况不太可能完全同时发生)

2.3.2 线程之间共享资源

在Python中,线程之间可以共享全局变量、堆上的对象或通过参数传递的共享数据结构来实现资源共享。

由于Python的全局解释器锁(GIL)的存在,对于CPU密集型任务,多线程可能不会像在多处理器系统上那样提供显著的并行性提升。

然而,对于I/O密集型任务,多线程仍然可以显著提高性能,因为等待I/O操作完成的线程可以被挂起,让出CPU给其他线程使用。

eg:展示了两个线程如何共享一个全局计数器变量

import threading

# 共享的全局变量
shared_counter = 0

# 定义线程要执行的函数
def increment_counter(thread_name, iterations):
    global shared_counter
    for _ in range(iterations):
        # 这里没有使用锁,因此存在竞态条件
        shared_counter += 1
        # 为了模拟一些工作,我们让线程休眠一小段时间
        # 在实际应用中,这可能是等待I/O操作完成
        time.sleep(0.001)  # 非常短的时间,仅用于演示
    print(f"{thread_name} incremented the counter {iterations} times.")

# 导入time模块(在上面的代码示例中遗漏了)
import time

# 创建线程
thread1 = threading.Thread(target=increment_counter, args=("Thread-1", 1000000))
thread2 = threading.Thread(target=increment_counter, args=("Thread-2", 1000000))

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

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

# 打印最终的计数器值
# 由于竞态条件,这个值可能不是预期的2000000
print(f"Final counter value: {shared_counter}")

注意:上面的代码示例存在竞态条件(race condition),因为两个线程在没有同步的情况下同时修改共享变量shared_counter。这意味着最终的计数器值可能不是预期的2,000,000,而是小于这个值,因为线程之间的操作可能会交错进行。

为了解决这个问题,我们可以使用threading.Lock来确保一次只有一个线程能够修改共享变量:

import threading
import time

# 共享的全局变量和锁
shared_counter = 0
counter_lock = threading.Lock()

# 定义线程要执行的函数
def increment_counter(thread_name, iterations):
    global shared_counter, counter_lock
    for _ in range(iterations):
        # 在修改共享变量之前获取锁
        with counter_lock:
            shared_counter += 1
        # 锁在with块结束时自动释放
        time.sleep(0.001)  # 模拟一些工作
    print(f"{thread_name} incremented the counter {iterations} times.")

# 创建线程
thread1 = threading.Thread(target=increment_counter, args=("Thread-1", 1000000))
thread2 = threading.Thread(target=increment_counter, args=("Thread-2", 1000000))

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

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

# 打印最终的计数器值
# 现在这个值应该是预期的2000000
print(f"Final counter value: {shared_counter}")

在这个修改后的示例中,我们使用了threading.Lock来确保对共享变量shared_counter的访问是线程安全的。with counter_lock:语句块确保了在修改计数器时,一次只有一个线程能够执行该块内的代码

2.3.3 资源竞争

资源竞争发生在多个线程尝试同时访问或修改共享资源时。每个线程都试图执行其代码,这可能涉及读取、写入或更新共享变量。如果没有适当的同步机制(如锁、信号量或条件变量),则一个线程可能会读取到另一个线程尚未完成写入的数据,或者两个线程可能会同时写入导致数据损坏。这种情况称为数据竞争或竞态条件。

eg:使用两个线程来递增一个共享的全局计数器,由于缺少同步机制,这两个线程可能会同时访问和修改计数器,导致最终的结果不正确。

import threading
import time

# 共享的全局计数器
shared_counter = 0

# 定义线程要执行的函数
def increment_counter(thread_name, iterations):
    global shared_counter
    for _ in range(iterations):
        # 这里没有使用锁,因此存在资源竞争
        # 线程可能会同时读取和写入shared_counter,导致数据损坏
        local_copy = shared_counter  # 读取共享变量
        local_copy += 1             # 在本地副本上进行修改
        shared_counter = local_copy  # 将修改后的值写回共享变量
        
        # 为了模拟一些工作,我们让线程休眠一小段时间
        # 在实际应用中,这可能是等待I/O操作完成
        time.sleep(0.00001)  # 非常短的时间,仅用于演示

    print(f"{thread_name} incremented the counter {iterations} times.")

# 创建线程
thread1 = threading.Thread(target=increment_counter, args=("Thread-1", 100000))
thread2 = threading.Thread(target=increment_counter, args=("Thread-2", 100000))

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

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

# 打印最终的计数器值
# 由于资源竞争,这个值可能不是预期的200000
print(f"Final counter value: {shared_counter}")

在这个例子中,increment_counter函数试图通过以下步骤来递增计数器:

1、读取共享变量shared_counter到本地变量local_copy

2、在local_copy上进行加1操作

3、将修改后的local_copy值写回共享变量shared_counter

然而,由于两个线程可能会同时执行这些步骤,它们可能会读取到相同的初始值,然后都将其加1,并写回相同的值。这会导致计数器只递增了一次,而不是两次。这种情况在多线程环境中会频繁发生,导致最终的计数器值远小于预期的200,000。

为了解决这个问题,我们需要使用同步机制,如锁(threading.Lock),来确保一次只有一个线程能够访问和修改共享变量。以下是使用锁来避免资源竞争的修改后的示例:

import threading
import time

# 共享的全局计数器和锁
shared_counter = 0
counter_lock = threading.Lock()

# 定义线程要执行的函数
def increment_counter(thread_name, iterations):
    global shared_counter, counter_lock
    for _ in range(iterations):
        # 在修改共享变量之前获取锁
        with counter_lock:
            # 锁保证了以下操作是原子的,即一次只有一个线程能够执行这些操作
            shared_counter += 1
        
        # 锁在with块结束时自动释放
        # 为了模拟一些工作,我们让线程休眠一小段时间
        time.sleep(0.00001)  # 非常短的时间,仅用于演示

    print(f"{thread_name} incremented the counter {iterations} times.")

# 创建线程
thread1 = threading.Thread(target=increment_counter, args=("Thread-1", 100000))
thread2 = threading.Thread(target=increment_counter, args=("Thread-2", 100000))

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

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

# 打印最终的计数器值
# 现在这个值应该是预期的200000
print(f"Final counter value: {shared_counter}")

今天的分享就到这里了,希望能够帮助到大家~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于机器视觉的表面缺陷检测

基于机器视觉的表面缺陷检测存在的问题与难点 - AVT相机|AVT红外相机|万兆网相机EVT|VIEWORKS线扫相|映美精相机|Specim多光谱相机|Adimec相机|Basler相机|富士能FUJINON镜头|理光RICOH镜头|OPTO远心镜头|SPO远心镜头|Navtar镜头|VST镜头|CCS光源|3D视觉引导机床上下料系统 (完…

SpringBoot整合MQTT利用EMQX完成消息的发布与接收+Python模拟硬件测试通信

教程说明 本教程主要内容为使用SpringBoot整合MQTT利用EMQX代理服务完成MQTT的消息发送与接收,然后用Python模拟硬件与SpringBoot应用进行了MQTT消息的通信,教程详细,并在最后讲解了开发中的注意事项,本教程适用于物联网领域、Ja…

IntelliJ IDEA 中,自动删除无用导包

在 IntelliJ IDEA 中,自动删除无用导包是一个提升代码整洁性和开发效率的重要功能。以下是实现这一功能的详细步骤: 一、通过快捷键手动删除无用导包 打开Java文件:在IDEA中打开你需要操作的Java文件。 使用快捷键: 在Windows系…

表格数据处理中大语言模型的微调优化策略研究

论文地址 Research on Fine-Tuning Optimization Strategies for Large Language Models in Tabular Data Processing 论文主要内容 这篇论文的主要内容是研究大型语言模型(LLMs)在处理表格数据时的微调优化策略。具体来说,论文探讨了以下…

如何编写一个 Vue 3 应用:模板插值示例

Vue.js 是一个渐进式的 JavaScript 框架,用于构建用户界面。在本篇博客中,我们将通过一个简单的示例来学习如何使用 Vue 3 创建一个基本的应用。这个示例将展示如何使用 Vue 的模板插值和事件处理来构建一个简单的点击计数器。 步骤 1: 准备工作 首先&…

基于混合ABC和A*算法复现

基于混合ABC和A*算法复现 一、背景介绍二、算法原理(一)A*算法原理(二)人工蜂群算法原理(三)混合ABC和A*算法策略 三、代码实现(一)数据准备(二)关键函数实现…

2024 APMCM亚太数学建模C题 - 宠物行业及相关产业的发展分析和策略(详细解题思路)

在当下, 日益发展的时代,宠物的数量应该均为稳步上升,在美国出现了下降的趋势, 中国 2019-2020 年也下降,这部分变化可能与疫情相关。需要对该部分进行必要的解释说明。 问题 1: 基于附件 1 中的数据及您的团队收集的额…

鸿蒙MVVM模式介绍与使用

文章目录 鸿蒙MVVM模式介绍与使用背景MVVM模式介绍相关装饰器介绍State状态变量Prop、Link的作用 MVVM架构模式的实现以及相关装饰器的使用具体实现效果 总结 鸿蒙MVVM模式介绍与使用 背景 最近在学习鸿蒙开发,想到了以前写安卓移动端应用时经常会用到的MVVM架构模式,就想着能…

Jmeter测试工具的安装和使用,mac版本,jmeter版本5.2.1

Jmeter测试工具的安装和使用JSON格式请求 一、安装1、安装jdk包和设置java环境2、去官网下载Jmeter3、解压后,打开mac终端,进入apache-jmeter的bin文件开启jmeter 二、使用jmeter1、添加线程2、添加HTTP请求3、配置请求的协议、IP地址、端口号、请求方法…

Spring Boot教程之十: 使用 Spring Boot 实现从数据库动态下拉列表

使用 Spring Boot 实现从数据库动态下拉列表 动态下拉列表(或依赖下拉列表)的概念令人兴奋,但编写起来却颇具挑战性。动态下拉列表意味着一个下拉列表中的值依赖于前一个下拉列表中选择的值。一个简单的例子是三个下拉框,分别显示…

Vue前端开发2.3.6 列表渲染指令

在Vue.js中,v-for指令是渲染列表的关键工具,支持从数组、对象、数字和字符串中生成列表。它简化了商品列表等重复元素的创建。为了优化性能,key属性为每个列表项提供唯一标识,帮助Vue追踪节点身份,实现元素重用。实战中…

敏捷开发02:敏捷开发之Scrum开发框架介绍

一、什么是 Scrum 1.1 Scrum 定义 Scrum 是敏捷开发方法之一,它使用比较广泛。 敏捷的其它开发方法还有 XP(极限编程)、FDD(特性驱动开发)、Crystal(水晶方法)、TDD(测试驱动开发)、DSDM(动态系统开发)等等敏捷方法。 Scrum-Guide 中定义的 Scrum: S…

使用mingw+CMake在Windows平台编译OpenCV

1. 安装mingw和cmake cmake的安装比较简单,百度一下完成相关操作即可,笔者安装的是3.24.3版本。 Mingw的安装也有很多相关文章,不过我使用的是安装QT时附带安装的mingw,其路径为D:\software\Qt\Tools\mingw1120_64。其中的bin文件…

能源电力企业安全数据内外网文件交换

在数字化浪潮的推动下,能源电力行业数据交换的频率急剧上升,涉及的视频、研发、设计等各类文件数量庞大。这些文件的内外网传输不仅要追求速度,更要确保数据安全。随着国家对数据安全重视程度的提高,《网络安全法》和《数据安全法…

突破内存限制:Mac Mini M2 服务器化实践指南

本篇文章,我们聊聊如何使用 Mac Mini M2 来实现比上篇文章性价比更高的内存服务器使用,分享背后的一些小的思考。 希望对有类似需求的你有帮助。 写在前面 在上文《ThinkPad Redis:构建亿级数据毫秒级查询的平民方案》中,我们…

C++ 二叉搜索树(Binary Search Tree, BST)深度解析与全面指南:从基础概念到高级应用、算法优化及实战案例

🌟个人主页:落叶 🌟当前专栏: C专栏 目录 ⼆叉搜索树的概念 ⼆叉搜索树的性能分析 ⼆叉搜索树的插⼊ ⼆叉搜索树的查找 二叉搜索树中序遍历 ⼆叉搜索树的删除 cur的左节点为空的情况 cur的右节点为空的情况 左,右节点都不为…

数据库、数据仓库、数据湖、数据中台、湖仓一体的概念和区别

数据库、数据仓库、数据湖、数据中台和湖仓一体是数据管理和分析领域的不同概念,各自有不同的特点和应用场景。以下是它们的主要区别: 1. 数据库(Database) 定义:结构化的数据存储系统,用于高效地存储、检…

RabbitMQ原理架构解析:消息传递的核心机制

文章目录 一、RabbitMQ简介1.1、概述1.2、特性 二、RabbitMQ原理架构三、RabbitMQ应用场景3.1、简单模式3.2、工作模式3.3、发布订阅3.4、路由模式3.5 主题订阅模式 四、同类中间件对比五、RabbitMQ部署5.1、单机部署5.2、集群部署(镜像模式)5.3、K8s部署…

idea_常用设置

相关设置 项目的JDK设置out目录取消自动更新设置主题设置菜单和窗口字体大小滚轮调节字体大小显示行号与方法分隔符代码智能提示忽略大小写自动导包配置设置项目文件编码设置控制台的字符编码修改类头的文档注释信息设置自动编译 项目的JDK设置 File -> Project Structure -…

Redis的管道操作

在现代应用程序中,Redis作为一种高性能的内存数据库,被广泛用于缓存、消息队列、实时分析等场景。为了进一步提高Redis的性能,Redis提供了管道(Pipeline)操作,允许客户端将多个命令一次性发送到服务器&…