Python并发编程——threading

news/2024/11/5 12:40:49 标签: python, java, linux, threading, 线程, 并发编程

目录

  • 1. 引言
  • 2. `threading` 的基本使用方法
  • 3. 线程锁与线程安全
    • 3.1 使用 `Lock` 对象
    • 3.2 使用 `RLock` 对象
  • 4. 线程池的使用
    • 4.1 使用线程池处理并发任务
    • 4.2 线程池任务的回调函数
  • 5. 死锁的处理
    • 5.1 死锁的演示
    • 5.2 解决方法

1. 引言

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

1.1 进程与线程的区别

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

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

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

threading__16">1.2 threading 模块的作用与优势

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

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


threading__24">2. threading 的基本使用方法

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

2.1 创建和启动线程

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

python">import threading

def 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,则线程被设置为守护线程,主线程结束时不会等待该线程

python">import threading
import time

def 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 来同步多个线程访问共享资源:

python">import threading

lock = threading.Lock()
shared_resource = 0

def increment_resource():
    global shared_resource
    with 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 来同步递归函数:

python">import threading

lock = 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 使用线程池处理并发任务

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

python">import time
from concurrent.futures import ThreadPoolExecutor

def 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() 方法,允许我们注册一个回调函数,当任务完成后立即执行该函数。

python">import time
from concurrent.futures import ThreadPoolExecutor

def 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 死锁的演示

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

python">import threading

lock1 = threading.Lock()
lock2 = threading.Lock()

def task1():
    with lock1:  # 线程1获取lock1
        print("Task 1 acquired lock1, trying to acquire lock2...")
        with lock2:  # 线程1尝试获取lock2
            print("Task 1 complete")

def task2():
    with lock2:  # 线程2获取lock2
        print("Task 2 acquired lock2, trying to acquire lock1...")
        with lock1:  # 线程2尝试获取lock1
            print("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.niftyadmin.cn/n/5739477.html

相关文章

【rust】rust基础代码案例

文章目录 代码篇HelloWorld斐波那契数列计算表达式(加减乘除)web接口 优化篇target/目录占用一个g,仅仅一个actix的helloWorld demo升级rust版本, 通过rustupcargo换源 代码篇 HelloWorld fn main() {print!("Hello,Wolrd&…

Python中如何计算整商:详解整除运算及其应用场景

目录 一、整除运算的基本概念 1. 语法 2. 工作原理 二、整除运算的详细解析 1. 整数之间的整除 2. 浮点数之间的整除 3. 整数与浮点数之间的整除 三、整除运算的应用场景 1. 数据处理中的取整操作 2. 循环中的步进控制 3. 分页显示数据 4. 时间计算中的取整 四、整…

鸿蒙HarmonyOS开发:给应用添加基础类型通知和进度条类型通知(API 12)

文章目录 一、通知介绍1、通知表现形式2、通知结构3、请求通知授权 二、创建通知1、发布基础类型通知2、发布进度类型通知3、更新通知4、移除通知 三、设置通知通道1、通知通道类型 四、创建通知组五、为通知添加行为意图1、导入模块。2、创建WantAgentInfo信息。4、创建WantAg…

HTTP、WebSocket、gRPC 或 WebRTC:各种协议的区别

在为您的应用程序选择通信协议时,有很多不同的选择。 本文将了解四种流行的解决方案:HTTP、WebSocket、gRPC 和 WebRTC。 我们将通过深入学习其背后原理、最佳用途及其优缺点来探索每个协议。 通信方式在不断改进:变得更快、更方便、更可靠&…

Source Insight 4.0常用操作

解决中文乱码问题 options->preferences_>file->

git clone,用https还是ssh

前言 在使用Git去克隆项目时,会遇到https和ssh等形式,这两种又有何种区别呢,本文将重点讨论在具体使用中的问题。 注:第一次使用Git 时,需要先设置全局用户名和邮箱,否则后续使用命令时会报错,也是提醒先添…

前端Nginx的安装与应用

目录 一、前端跨域方式 1.1、CORS(跨域资源共享) 1.2、JSONP(已过时) 1.3、WebSocket 1.4、PostMessage 1.5、Nginx 二、安装 三、应用 四、命令 4.1、基本操作命令 4.2、nginx.conf介绍 4.2.1、location模块 4.2.2、反向代理配置 4.2.3、负载均衡模块 4.2.4、通…

关于金属氢化物(储氢)PCT曲线拟合、ZBS有效导热系数模型、JMAK类型吸放氢动力学方程的笔记

参考文献:Experimental and numerical study of metal hydride beds with Ti0.92Zr0.10Cr1.0Mn0.6Fe0.4 alloy for hydrogen compressionhttps://www.sciencedirect.com/science/article/pii/S1385894723043851?via%3Dihub#s0010 一、PCT曲线拟合 根据以下文献内容…