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()
会使得主线程在等待线程完成时无法立即响应。(解决:在相应中断信号时,可以再exceptKeyboardInterrupt
块中添加对线程清理或终止逻辑,确保资源被正确处理)
-
-
某些情况可以不使用
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主服务关闭")