Python编程:深入探索线程实战技巧

Python编程:深入探索线程技巧

一、线程的基础知识

1.线程的核心概念

  • 线程的基础概念:操作系统能够调度的最小执行单位,是进程中的一个独立控制流,能够独立运行被中断恢复运行

  • 线程的特性:线程之间可以更轻松的共享数据和通信,因为它们属于同一个进程,共享相同的地址空间

  • 线程的由来

    • 背景:早期操作系统以进程为最小调度单位,但进程创建/切换开销大(需要独立的内存空间)
    • 动机提高并发效率,减少资源消耗。"轻量级进程"应运而生,共享进程资源。

2.线程的运行原理

a.线程的主要结构

  • 线程控制块(TCB)

    • 作用:线程的"身份证",存储线程所有关键信息
    • 内容
      • 线程标识器:唯一标识线程
      • 寄存器状态:程序计数器(记录一下条指令的代码段存储的地址)、栈指针等
      • 栈空间:用于存储局部变量和函数调用链条(程序执行过程中,函数之间按顺序相互调用的层级关系)
      • 状态标识:就绪/执行/阻塞/终止等
      • 优先级
      • 线程上下文:切换时保存CPU执行环境
      • 所属进程PID:表明归属关系
  • 线程栈(Stack)

    • 独立栈空间:每个线程拥有独立的调用栈,所有线程的栈都位于进程的虚拟地址空间内,属于进程资源的一部分。
    • 存储内容
      • 函数参数和局部变量
      • 函数调用返回地址
      • 中断时的临时数据
    • 大小限制:典型值8MB,(可配置,超出会导致栈溢出)
  • 寄存器

    • 程序计数器:指向下条待执行指令
    • 栈指针:指向当前栈顶位置
    • 通用寄存器:保存运算中间结果
    • 特权寄存器:如进程控制相关的寄存器
  • 线程的状态

    • 新建:初始化,也就是申请TCB
    • 就绪:等待CPU调度
    • 运行:正在执行
    • 阻塞:等待I/O等资源
    • 终止:执行完成

b.线程占用的资源问题

  • 共享资源(同一个进程的地址空间)

    • 文本段
    • 数据段
    • 堆内存
  • 独有资源

    • 栈内存
    • 寄存器状态
    • 错误码、线程私有数据等

c.运行原理

  • 操作系统调度:内核通过时间片轮转算法分配CPU时间,每个线程轮流执行。
  • 上下文切换:保存当前线程状态(寄存器、程序计数器),加载下一个线程状态。
  • 线程状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、终止(Terminated)。
  • 多核并行:在多核CPU中,不同线程可分配到不同核心上真正并行执行

3.线程的协调和功能

a.线程间的协调

  • 共享内存:直接读写进程的全局变量和堆内存
  • 同步机制
    • 互斥锁:确保同一时间仅有一个线程访问临界区
    • 条件变量:线程等待特定条件满足后唤醒
    • 信号量:控制并发访问资源数量
    • 读写锁:区分读写操作,提升并发效率

b.线程的功能

  • 提高效率:利用CPU空闲时间(如等待I/O时切换线程)。
  • 简化设计:将复杂任务拆分为多个线程独立处理(如服务器同时处理多个客户端请求)。
  • 实时响应:主线程保持界面响应,后台线程处理耗时操作。

4.线程的分类

  • 内核级线程用户级线程

    • 内核级线程:由操作系统内核直接管理的线程
      • 它拥有自己的系统资源调度策略
    • 用户级线程:在用户空间中实现的线程
      • 它不需要内核的支持,但它需要用户程序自己实现线程的管理和调度

    现代操作系统通常采用混合线程模型

a.内核级线程

  • 原理:内核线程模型中,会将TCB放到内核空间中,线程的创建、终止、切换等都要经过系统的调用方式来执。

  • 定义:由操作系统内核直接管理和调度的先成功,内核感知其存在并未每个线程分配资源

  • 特点:

    • 内核参与:创建、销毁、调度由内核完成
    • 多核并行:不同内核线程可分配到多个CPU核心,实现真正的并行
    • 阻塞安全:一个线程阻塞(如I/O操作)时,其他线程仍可运行。
    • 资源开销大:内核需为每个线程维护元数据(如线程控制块TCB),频繁切换导致性能损耗。
  • 应用场景:

    • 需要高并发且需利用多核性能的任务(如科学计算、视频渲染)
    • 需要避免单一线程阻塞影响整体进程的任务(实时系统

b.用户级线程

  • 定义:完全在用户空间(线程库)实现的线程,内核无感知,仅将整个进程视为单一的调度空间

  • 特点:

    • 用户级管理:由用户态线程库创建和调度,无需内核介入
    • 轻量高效:切换无需内核态切换,开销极低,可创建大量的线程
    • 无法并行:内核仅调度进程,所有用户线程都绑定单一内核线程,无法利用多核
    • 阻塞传染:一个线程阻塞(如系统调用)会导致整个进程阻塞(所有用户线程都会被挂起)
  • 应用场景

    • 高并发但计算密度低的任务(如web服务器处理请求)
    • 资源受环境或需要极高的高频线程切换的场景

c.内核线程vs用户线程

特性内核线程用户线程
管理主体操作系统内核用户态线程库
创建/销毁等开销
多核并行能力支持不支持
线程阻塞的影响仅阻塞当前线程阻塞整个进程的所有用户线程
资源消耗

d.用户线程与内核线程的关系模型

  • 一对一模型:每个用户线程绑定一个内核线程(如现代Linux的NPTL库)。
    • 优点:支持多核并行,阻塞安全。
    • 缺点:线程数量受内核限制,创建开销大。
  • 多对一模型:多个用户线程复用一个内核线程。
    • 优点:轻量高并发。
    • 缺点:无法多核并行,单一线程阻塞导致进程阻塞。
  • 多对多模型(混合模型):用户线程动态绑定到多个内核线程。
    • 优点:平衡并发与并行(如Go语言的GMP调度模型)。

二、python中的线程实现

Python中的线程是通过标准库threading模块实现的,其地城本质上是内核线程,但是由于Python全局解释器锁的限制,它的行为在某些场景下表现更像用户线程

  • GIL的干扰

    • 虽然Python线程是内核线程,但是由于GIL的存在,多线程无法真正的并行执行Python字节码。

    • GIL的作用:GIL是Cpython及解析器中的一个互斥锁,用于python对象内存安全。同一时刻,只有一个线程可以持有GIL并执行Python字节码。

    • 线程切换机制

      • 当一个线程运行一段时间后,会主动释放GIL,触发线程切换。
      • 在I/O操作时,线程也会释放GIL,允许其他线程执行。
    • 结果:即使由多个内核的线程,同一进程内的Python线程在CPU密集任务中只能交替执行,无法进行多核并行。

  • 绕过GIL限制的方法

    • 多进程:每个进程由独立的GIL,可以并行利用多核
    • 使用其他解释如Jython(无GIL)
    • C/C++扩展:在C扩展中释放GIL,执行计算密集型代码
    • 异步编程:单线程内荣国协程处理高并发I/O任务。

1.代码实现基础

核心模块:threading

a.创建线程(两种方式)

# 案例:创建线程

# 方式1:函数传递
def task():
    print('thread is running')
t1 = threading.Thread(target=task)
t1.start()          # 执行


# 方式2:基础Thread类
class MyThread(threading.Thread):
    def run(self) -> None:
        print('custom thread is runing')
t2 = MyThread()   # MyThread类的实例
t2.start()		  # 执行

b.启动和等待

t1.start()  # 启动线程
t1.join()   # 主线程等待t1结束,也就是等待子线程结束,而此时主线程会被挂起。
  • join() 作用

    • 主线程等待t1结束,也就是等待子线程结束,而此时主线程会被阻塞,直到子线程完成。

    • 更好的资源管理,确保所有资源都会被正确释放,数据完成

    • 坏处

      • 主线程阻塞
      • 程序终止不灵活:如果主线程需要响应中断并立即终止程序,使用 join() 会使得主线程在等待线程完成时无法立即响应。(解决:在相应中断信号时,可以再except KeyboardInterrupt块中添加对线程清理或终止逻辑,确保资源被正确处理)
  • 某些情况可以不使用join()

    • 子线程不是关键任务,可以随着主线程结束时安全中断,使用守护线程daemon

c.同步机制

  • 锁(Lock):防止多个线程同时修改共享资源

    lock = threading.Lock()
    with Lock:
        # 临界区代码
    
  • 信号量:控制同时访问资源的线程数量

  • 事件:线程间通信,通过信号触发行为

d.守护线程

t = threading.Thread(target=task, daemon=True)
# 主线程退出时,守护线程自动终止

2.案例

a.I/O密集型任务(多线程有效)

# 案例:多任务文件下载

import threading
import time
import functools

total_time = 0


def func_run_time(func):
    @functools.wraps(func)
    def wrapper(*args):
        start_time = time.perf_counter()
        func(*args)
        end_time = time.perf_counter()
        took_time = end_time - start_time
        print(f'func {func.__name__}  took totoal {took_time}秒')

    return wrapper


def download(url):
    print(f"开始下载{url}")
    time.sleep(2)  # 模拟I/O 等待
    print(f"下载完成{url}")


@func_run_time
def solo_thread():
    urls = []
    for i in range(10):
        url = f"https://example{i}.com"
        urls.append(url)
    print(urls)
    # 单线程执行
    for url in urls:
        download(url)


@func_run_time
def multi_thread():
    urls = []
    for i in range(10):
        url = f"https://example{i}.com"
        urls.append(url)
    # 多线程执行
    threads = []
    for url in urls:
        t = threading.Thread(target=download, args=(url,))
        t.start()
        threads.append(t)

    for t in threads:
        t.join()


# 单线程
solo_thread()

print('\n============多线程======================')
# 多线程
multi_thread()

"""
	单线程:运行10个下载任务在20秒左右完成
	多线程:运行10个下载任务在2秒左右完成
"""

b.CPU密集型任务(受GIL限制)

# 计算10^7内的相加之和 (两次) 使用线程无任何提速

import threading
import time


def calculate():
    sum = 0
    for _ in range(10 ** 7):
        sum += 1


# 单线程
start = time.time()
calculate()
calculate()
print(f"单线程耗时: {time.time() - start:.2f}s")  # 约0.41s

# 多线程
t1 = threading.Thread(target=calculate)
t2 = threading.Thread(target=calculate)

start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
print(f"多线程耗时: {time.time() - start:.2f}s")  # 约0.4s(未提速)

c.实时日志监控(守护线程)

# 案例场景:主程序运行期间,后台线程持续监控日志文件变化

import threading
import time

class LogMonitor(threading.Thread):
    def __init__(self, file_path):
        super().__init__()
        self.file_path = file_path
        self.daemon = True  # 设置为守护线程(主程序退出自动结束)
        self.last_position = 0

    def run(self):
        print("日志监控启动")
        while True:
            with open(self.file_path, "r") as f:
                f.seek(self.last_position)
                new_logs = f.read()
                if new_logs:
                    print("发现新日志:", new_logs.strip()) 
                self.last_position = f.tell()  # 获取当前的位置
            time.sleep(1)  # 每秒检查一次

# 主程序
monitor = LogMonitor("/var/log/myapp.log")
monitor.start()

# 模拟主程序工作
print("主程序开始运行...")
for i in range(5):
    print(f"主程序处理任务 {i}")
    time.sleep(2)

print("主程序退出,守护线程自动终止")
  • seek(offset, whence) 是文件对象的方法,其中有两个参数:

    • offset: 相对于 whence 的偏移量,以字节为单位。

    • whence
      指定偏移的参考位置。它有三个可能的值:
      • 0:文件的开头(默认值)
      • 1:当前位置
      • 2:文件的结尾
  • tell() 方法不接受任何参数,并且返回一个整数值,表示文件指针相对于文件开头的当前位置(以字节为单位)

d.批量图片下载 + 线程池

# 案例场景:从url列表下载大量图片,限制最大并发数,避免服务器压力过大
import concurrent.futures
import requests
import os

def download_image(url, save_dir="images"):
    if not os.path.exists(save_dir): # 判断文件夹是否存在,如果没有就创建images
        os.makedirs(save_dir)
        
    filename = url.split("/")[-1]   # 文件名以最后一个/匹配的(img1.jpg)
    path = os.path.join(save_dir, filename)
    
    response = requests.get(url)   # 执行请求
    with open(path, "wb") as f:    # 保存response.content
        f.write(response.content)
    return path

# 模拟10个图片URL
image_urls = [
    "http://example.com/img1.jpg",
    "http://example.com/img2.jpg",
    # ... 添加更多URL
]

# 使用线程池(最大并发5线程)
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    futures = []
    for url in image_urls:
        futures.append(executor.submit(download_image, url))
    
    # 获取完成结果
    for future in concurrent.futures.as_completed(futures):
        try:
            print(f"图片已保存至: {future.result()}")
        except Exception as e:
            print(f"下载失败: {e}")

e.定时任务心跳检测

# 案例场景:每个30秒检测服务是否还存活,主线程不阻塞


# 案例场景:每个30秒检测服务是否还存活,主线程不阻塞(不使用join方法)
import time
import threading


def heartbeat_check():
    while True:
        print('[心跳检测] 服务状态正常')
        time.sleep(30)


t1 = threading.Thread(target=heartbeat_check(), daemon=True)  # 守护线程开启
t1.start()

# 主线程继续工作
print("主服务启动...")
try:
    while True:
        print("主服务运行中...")
        time.sleep(5)
except KeyboardInterrupt:
    print("\n主服务关闭")

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值