Python并发编程——threading

目录

  • 1. 引言
    • 1.1 进程与线程的区别
    • 1.2 `threading` 模块的作用与优势
  • 2. `threading` 的基本使用方法
    • 2.1 创建和启动线程
    • 2.2 守护线程与非守护线程
  • 3. 线程锁与线程安全
    • 3.1 使用 `Lock` 对象
    • 3.2 使用 `RLock` 对象
  • 4. 线程池的使用
    • 4.1 使用线程池处理并发任务
    • 4.2 线程池任务的回调函数
  • 5. 死锁的处理
    • 5.1 死锁的演示
    • 5.2 解决方法

1. 引言

在 Python 中,为了实现并发执行任务,开发者可以选择使用线程或进程来提升程序性能并减少等待时间。threading 模块是 Python 提供的用于创建和操作线程的标准库。线程是程序执行的最小单位,而进程是系统资源分配的基本单位。理解线程与进程的区别是深入学习多线程编程的基础。

1.1 进程与线程的区别

为了更好地理解线程和进程,我们可以用一个生动的例子来解释它们之间的区别:将一个计算机程序想象成一个工厂,而该工厂中的不同车间代表不同的进程。每个车间可以独立生产不同的产品(即执行不同的任务),彼此之间相互隔离。线程则是每个车间中的工人,一个车间(进程)可以有多个工人(线程)同时工作,他们共享车间内的资源,比如原材料和工具(内存和文件句柄等)。

例如,当一个用户同时打开多个应用程序时,每个应用程序都是一个独立的进程。每个应用程序内部可能有多个并发任务在执行,如文件读取、UI 刷新等,这些任务由各自的线程负责处理。

  • 进程:是独立的执行单元,每个进程都有自己独立的内存空间。进程间通信较为复杂,但它们的隔离性可以提高程序的稳定性。
  • 线程:是共享相同内存空间的多个执行单元。由于共享资源,线程之间的通信更简单,但也更容易引发数据竞争和资源冲突。

1.2 threading 模块的作用与优势

Python 的 threading 模块用于实现多线程编程,允许多个线程在同一进程中并发执行任务。这对于 I/O 密集型任务非常有效,例如文件读取、网络请求等。在这种情况下,使用 threading 可以提高程序的响应速度和吞吐量。

⚠️ 由于 Python 的全局解释器锁(GIL)的存在,多线程在进行 CPU 密集型任务时效果有限,因为 GIL 会限制线程的并发执行。这是 Python 线程的一个局限性。


2. threading 的基本使用方法

threading 模块为 Python 程序提供了创建和管理线程的简便方式。在深入探讨复杂的多线程操作之前,了解线程的基本使用是至关重要的。

2.1 创建和启动线程

在 Python 中,可以通过 threading.Thread 类轻松创建和启动线程。

import threadingdef print_hello():print("Hello from thread")# 创建线程
t = threading.Thread(target=print_hello)
t.start()  # 启动线程
t.join()   # 等待线程结束
  • Threadthreading.Thread 是创建线程的核心类。通过传入 target 参数,我们可以指定线程启动时需要运行的函数。
  • start() 方法:用于启动线程,调用后线程开始运行 target 中指定的函数。
  • join() 方法:用于阻塞主线程,直到调用 join() 的线程执行完毕。这样可以确保主线程在退出前等待所有非守护线程完成工作。

在一个简单的 web 抓取应用中,使用线程可以让每个页面的下载并发进行,从而减少总耗时。例如,我们可以创建多个线程,每个线程负责下载不同网页的内容。

2.2 守护线程与非守护线程

Python 的线程分为守护线程和非守护线程。默认情况下,线程是非守护的,意味着主线程会等待所有非守护线程结束后再退出。如果将 daemon 属性设置为 True,则线程被设置为守护线程,主线程结束时不会等待该线程。

import threading
import timedef task():time.sleep(5)print("Task complete")t = threading.Thread(target=task)
t.daemon = True  # 通过属性设置守护线程
t.start()print("Main thread ends")

在此示例中,主线程在打印 “Main thread ends” 后不会等待 task 函数完成,而是直接结束,task 线程将随之终止。

📝 守护线程适合于执行一些后台任务,例如日志记录、数据同步等。这些任务即使在主程序结束时未完成,也不会影响程序的主要功能。

3. 线程锁与线程安全

在多线程编程中,共享数据访问是提高程序性能的常用方法,但它也引入了数据竞争和不一致性的风险。由于多个线程能够并发地访问和修改共享数据,这种情况可能会导致数据紊乱,甚至程序崩溃。因此,为了保障数据的一致性和安全性,必须采用线程同步机制。LockRLock 是 Python 中提供的两种常用的线程同步工具。

3.1 使用 Lock 对象

Lock 对象是 Python 标准库中用于线程同步的基础组件。它通过显式的加锁与解锁机制,确保在同一时间只有一个线程可以访问临界区(critical section),从而避免数据竞争。Lock 提供了 acquire()release() 方法来管理锁的获取与释放。

以下是一个简单示例,演示如何使用 Lock 来同步多个线程访问共享资源:

import threadinglock = threading.Lock()
shared_resource = 0def increment_resource():global shared_resourcewith lock:  # 自动处理锁的获取与释放for _ in range(100000):shared_resource += 1# 创建多个线程
t1 = threading.Thread(target=increment_resource)
t2 = threading.Thread(target=increment_resource)# 启动线程
t1.start()
t2.start()# 等待线程完成
t1.join()
t2.join()print(shared_resource)
  • with lock::使用上下文管理器 with 可以自动调用 acquire()release(),确保即使在出现异常时锁也能正确释放,避免死锁。
  • 线程同步Lock 确保只有一个线程可以进入临界区,从而防止同时对共享数据的访问和修改,保证数据的一致性。

线程同步对于一些关键场景至关重要。例如,在一个涉及账户转账的程序中,如果多个线程尝试同时读取和修改账户余额,可能会导致账户数据不一致。通过使用 Lock,我们可以确保每次只有一个线程访问和更新账户信息,避免数据竞态。

📝 如果在某些复杂情况下,锁未被正确释放,程序可能会进入死锁状态。因此,使用 with lock: 是一种推荐的写法。

3.2 使用 RLock 对象

RLock(递归锁)是 Lock 的递归版本,允许同一线程多次获取锁而不会发生死锁。对于需要在同一线程内多次调用锁(例如递归函数或嵌套的临界区),RLock 是理想的选择。

下面展示了如何使用 RLock 来同步递归函数:

import threadinglock = threading.RLock()def recursive_task(count):if count > 0:with lock:print(f"Recursing {count}")recursive_task(count - 1)# 创建并启动线程
t = threading.Thread(target=recursive_task, args=(5,))
t.start()
t.join()
  • 递归场景RLock 允许同一线程多次调用 acquire() 而不会阻塞自己,避免了因重复锁定导致的死锁。
  • 安全性RLock 内部维护了一个计数器,只有当所有 acquire() 都被 release() 匹配时,锁才会真正释放。

RLock 在涉及多级锁嵌套的代码中尤为有用。例如,当一个线程已经获取了锁并试图再次获取时,如果使用普通的 Lock,将会导致死锁。而使用 RLock,则允许线程递归地获取和释放锁。

4. 线程池的使用

在并发编程中,线程管理和任务调度是复杂而又至关重要的环节。Python 3 引入的 concurrent.futures 模块,特别是 ThreadPoolExecutor 类,极大地简化了线程池的使用。ThreadPoolExecutor 提供了灵活、高效的接口,便于同时处理大量并发任务并提升程序性能。

4.1 使用线程池处理并发任务

线程池是一种管理一组可重用线程的机制,避免了为每个任务创建和销毁线程所带来的开销。使用线程池时,我们不需要手动管理线程的生命周期,只需专注于任务逻辑和调度。

import time
from concurrent.futures import ThreadPoolExecutordef task(n):time.sleep(1)  # 模拟耗时操作return f"Task {n} complete"# 创建一个最多包含5个线程的线程池
with ThreadPoolExecutor(max_workers=5) as executor:# 提交10个任务到线程池futures = [executor.submit(task, i) for i in range(10)]# 获取并打印每个任务的结果for future in futures:print(future.result())
  • ThreadPoolExecutor:这是一个高层接口,用于管理线程池的创建、任务调度和线程的回收。max_workers 参数指定线程池中的最大线程数,通常根据任务的性质和系统资源进行调整。
  • submit() 方法:将任务提交到线程池进行异步执行,返回一个 Future 对象。Future 是一种代表异步操作的占位符,可用于检查任务状态或获取任务结果。
  • 自动调度:线程池会根据任务数量和 max_workers 参数,自动分配和调度线程来执行任务,提高了程序的并发处理能力。

线程池适用于需要并发处理大量任务的场景,如 I/O 密集型任务(文件读写、网络请求等)和需要进行大量异步调用的程序。例如,在 Web 爬虫、日志处理和数据分析中,线程池能够显著提高程序的处理速度和响应能力。

4.2 线程池任务的回调函数

在某些情况下,我们希望在任务完成后执行特定的后续操作。这时可以使用回调函数来自动处理任务结果。Future 对象提供了 add_done_callback() 方法,允许我们注册一个回调函数,当任务完成后立即执行该函数。

import time
from concurrent.futures import ThreadPoolExecutordef task(n):time.sleep(1)return f"Task {n} complete"# 定义一个回调函数,用于处理任务结果
def callback(future):print(future.result())with ThreadPoolExecutor(max_workers=5) as executor:# 提交任务并为每个任务添加回调函数futures = [executor.submit(task, i) for i in range(10)]for future in futures:future.add_done_callback(callback)
  • add_done_callback() 方法:此方法用于在任务完成时调用指定的回调函数。回调函数接收 Future 对象作为参数,可以通过 future.result() 获取任务的返回值。
  • 任务链:通过回调函数,我们可以实现任务链,例如任务完成后触发后续任务,或进行结果处理、日志记录等操作。

回调函数在需要处理任务结果的场景中非常有用。例如,当多线程任务用于计算、数据处理或网络请求时,回调函数可以将结果直接传递给数据存储模块,或触发通知机制,无需在主线程中显式地轮询任务状态。

5. 死锁的处理

在多线程编程中,死锁是一个严重的问题,它指的是两个或多个线程互相等待对方释放资源,从而陷入一种永久的等待状态。这种情况会导致程序无法继续执行,影响系统的稳定性和性能。因此,理解死锁的产生原因以及有效的解决方法是至关重要的。

5.1 死锁的演示

为了更好地理解死锁,我们可以通过一个简单的示例来演示这一现象。以下代码展示了两个线程如何因锁的获取顺序不同而产生死锁。

import threadinglock1 = threading.Lock()
lock2 = threading.Lock()def task1():with lock1:  # 线程1获取lock1print("Task 1 acquired lock1, trying to acquire lock2...")with lock2:  # 线程1尝试获取lock2print("Task 1 complete")def task2():with lock2:  # 线程2获取lock2print("Task 2 acquired lock2, trying to acquire lock1...")with lock1:  # 线程2尝试获取lock1print("Task 2 complete")t1 = threading.Thread(target=task1)
t2 = threading.Thread(target=task2)
t1.start()
t2.start()
t1.join()
t2.join()

在这个示例中,task1 函数首先获取 lock1,然后尝试获取 lock2;而 task2 则先获取 lock2,再尝试获取 lock1。当 task1task2 分别获取了对方所需的锁时,它们都在等待对方释放锁,导致程序卡死,无法继续执行。

🧑‍💻 死锁的发生通常可以归结为几个条件:互斥条件、持有并等待、不可抢占和循环等待。在我们的示例中,互斥条件和持有并等待条件都满足,导致了死锁的发生。

5.2 解决方法

为了避免死锁的发生,可以采取多种策略,其中一种有效的方法是使用锁的超时参数。通过设置一个超时值,线程在等待获取锁时,如果超过了这个时间就会放弃获取锁,从而避免陷入永久等待的状态。例如,在 Python 中,可以通过 lock.acquire(timeout=3) 来尝试在三秒内获取锁,如果未能获取到锁,线程将继续执行,避免死锁。

此外,另一种常见的解决策略是重新设计锁的获取顺序。在多线程程序中,确保所有线程都以相同的顺序获取锁,可以有效避免循环等待的情况,从而减少死锁的风险。例如,如果所有线程都首先尝试获取 lock1,然后再获取 lock2,就可以避免死锁的发生。

在更复杂的场景中,还可以采用更高级的算法,如银行家算法,通过对资源分配进行精细控制,确保系统的安全状态,从而避免死锁。

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Golang--数组、切片、映射

1、数组 1.1 数组类型 var 数组名 [数组大小]数据类型 package main import "fmt"func main(){//1、定义一个数组var arr1 [5]intarr1[0] 100arr1[1] 200fmt.Println(arr1) //[100 200 0 0 0] } 1.2 数组的初始化方式 package main import "fmt" func …

结构体对齐,位段

大家好,今天来给大家分享一些结构体的知识,结构体是我们学习数据结构的基础,只有把它了解清楚才能让我们学习数据结构是得心应手,现在让我们来看看它的一些内容吧。 1.结构体的定义和调用我们就跳过吧 大家如果还不熟悉的话可以去…

ElementUI中el-table双击单元格显示输入框

效果图 实现 <el-table:data"formData.products"row-key"id":show-header"true"style"width: 100%; margin-top: 16px"class"zq-table-theme-info"bordercell-dblclick"handleDbClick"> <el-table-col…

Python OpenCV 图像改变

更改图像数据 通过 改像素点 或者 切片的区域 import cv2 import numpy as np img cv2.imread("image.jpg") print(img[3,5]) # 显示某位置(行3列5)的像素值( 如 [53 34 29] 它是有三通道 B G R 组成) img[3,5] (0,0,255) # 更改该位置的像素…

学习虚幻C++开发日志——定时器

官方文档&#xff1a;虚幻引擎中的Gameplay定时器 | 虚幻引擎 5.5 文档 | Epic Developer Community | Epic Developer Community 定时器 安排在经过一定延迟或一段时间结束后要执行的操作。例如&#xff0c;您可能希望玩家在获取某个能力提升道具后变得无懈可击&#xff0c;…

网络安全设备Bypass功能介绍及分析

网络安全平台厂商往往需要用到一项比较特殊的技术&#xff0c;那就是Bypass&#xff0c;那么到底什么是Bypass呢&#xff0c;Bypass设备又是如何来实现的&#xff1f;下面我就对Bypass技术做一下简单的介绍和说明。 一、 什么是Bypass。 大家知道&#xff0c;网络安全设备一般…

如何更改Android studio的项目存储路径

如果你希望永久更改Android Studio的默认项目保存路径&#xff0c;可以通过以下步骤进行设置&#xff1a; 打开Android Studio&#xff0c;选择“File”菜单下的“Settings”&#xff08;Windows&#xff09;或“Preferences”&#xff08;Mac&#xff09;。在设置窗口中&…

ESP8266 自定义固件烧录-mqtt透传固件

esp8266 mqtt固件配网及使用说明_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV196421G7Xc/?spm_id_from333.999.0.0一、固件介绍 固件为自定义开发的一个适配物联网项目的开源固件&#xff0c;支持网页配网、支持网页mqtt服务器配置、支持主题设置。 方便、快捷、稳…

二十三、Mysql8.0高可用集群架构实战

文章目录 一、MySQL InnoDB Cluster1、基本概述2、集群架构3、搭建一主两从InnoDB集群3.1、 安装3个数据库实例3.2、安装mysqlrouter和安装mysqlshell3.2.1、安装mysql-router3.2.2、安装mysql-shell 3.3、InnoDB Cluster 初始化3.1 参数及权限配置预需求检测3.2 初始化InnoDB …

[OS] mmap() 函数的参数及其作用

参数说明&#xff1a; addr&#xff1a;映射区域的起始地址。如果设置为 0&#xff0c;则由内核自动选择页对齐的地址。length&#xff1a;需要映射的字节数&#xff0c;决定映射的区域大小。prot&#xff1a;映射区域的内存保护属性&#xff0c;如只读、可读写等。这个属性不…

meta-learning based FD论文阅读笔记

[1]Semi-Supervised Temporal Meta-Learning Framework for Wind Turbine Bearing Fault Diagnosis Under Limited Annotation Data 问题背景 the fault data are so scarce that it is time-consuming to acquire a well behaved deep learning modelmuch unlabeled data ca…

web渗透——小白入狱

目录 理论知识总结一、Web渗透核心知识点二、Web渗透实操案例三、Web渗透学习建议实操案例一、信息收集实操步骤&#xff1a; 二、SQL注入实操步骤&#xff1a; 三、跨站脚本攻击&#xff08;XSS&#xff09;实操步骤&#xff1a; 四、CSRF攻击实操步骤&#xff1a; 五、本地文…

一个完整的产品级物联网系统在农业领域的应用,通过传感器、通信、云计算和控制设备的协同工作,实现了智能化的农业灌溉管理

以下为您详细介绍一个智能农业灌溉系统作为产品级的物联网实际案例&#xff1a; **一、项目背景** 随着农业现代化的发展&#xff0c;精准灌溉对于提高农作物产量、节约水资源具有重要意义。传统的灌溉方式往往依赖人工经验&#xff0c;效率低下且浪费水资源。因此&#xff0c…

JeecgBoot入门

最近在了解低代码平台&#xff0c;其中关注到gitee上开源项目JeecgBoot&#xff0c;JeecgBoot官方也有比较完整的入门教学文档&#xff0c;这里我们将耕者官方教程学习&#xff0c;并将其记录下来。 一、项目简介 JeecgBoot 是一款基于代码生成器的低代码开发平台拥有零代码能力…

qt QEvent详解

1、概述 QEvent是Qt框架中事件机制的基础类。在Qt中&#xff0c;事件是由底层窗口系统&#xff08;如Windows、Linux的X11、macOS的Cocoa等&#xff09;生成的&#xff0c;Qt的主事件循环&#xff08;QCoreApplication::exec()&#xff09;负责从事件队列中获取这些事件&#…

#Jest进阶知识:整合 webpack 综合练习

这一小节&#xff0c;我们来做一个综合的练习&#xff0c;该练习会整合&#xff1a; typescriptwebpackjest 准备工作 首先创建项目目录&#xff0c;通过 npm init -y 进行初始化。 整个项目我们打算使用 typescript 进行开发&#xff0c;因此需要安装 typescript npm i t…

【安卓13 源码】Input子系统(4)- InputReader 数据处理

1. 多指触控协议 多指触控协议有 2 种&#xff1a; > A类&#xff1a; 处理无关联的接触&#xff1a; 用于直接发送原始数据&#xff1b; > B类&#xff1a; 处理跟踪识别类的接触&#xff1a; 通过事件slot发送相关联的独立接触更新。 B协议可以使用一个ID来标识触点&…

VMware 虚拟机使用教程及 Kali Linux 安装指南

VMware 虚拟机使用教程及 Kali Linux 安装指南 在现代计算机科学与网络安全领域&#xff0c;虚拟化技术的应用越来越广泛。VMware 是一款功能强大的虚拟化软件&#xff0c;可以帮助用户在同一台物理机上运行多个操作系统。本文将详细介绍如何使用 VMware 虚拟机&#xff0c;并…

工业通信网关的各项功能解析-天拓四方

在工业自动化和智能制造的浪潮中&#xff0c;工业通信网关作为连接工业现场与互联网的重要桥梁&#xff0c;发挥着至关重要的作用。它不仅实现了不同网络协议之间的转换&#xff0c;还在数据采集、设备控制、网络管理等方面展现出强大的功能。 一、协议转换功能 工业通信网关…

用Python打造媒体管理播放器:从零到全功能GUI应用

背景 在日常生活中&#xff0c;我们经常需要管理和播放大量媒体文件。市面上的音频播放器可能功能单一&#xff0c;或者界面复杂。作为一名程序员&#xff0c;我决定使用Python自己打造一个简单yet强大的媒体管理播放器。 C:\pythoncode\new\playsong.py 全部代码 import os…