13 KiB
操作系统线程理论
进程
进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
线程
60年代,在 OS 中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端:
- 进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程。
- 对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
因此在 80 年代,出现了能独立运行的基本单位:线程(Threads)。进程是资源分配的最小单位,线程是CPU调度的最小单位,每一个进程中至少有一个线程。
进程和线程的关系
线程与进程的区别可以归纳为以下4点:
- 地址空间和其它资源共享(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
- 通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信,需要进程同步和互斥手段的辅助,以保证数据的一致性。
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
- 多线程操作系统中,进程不是一个可执行的实体。
使用线程的实际场景
开启一个打字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。
内存中的线程
线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:
- 父进程有多个线程,那么开启的子线程是否需要同样多的线程
- 在同一个进程中,如果一个线程关闭了文件,而另外一个线程正准备往该文件内写内容呢?
因此,在多线程的代码中,需要更多的心思来设计程序的逻辑、保护程序的数据。
python 线程使用
全局解释器锁 GIL
Python 代码的执行由 Python 解释器主循环控制。Python 在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。对 Python 解释器的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
在多线程环境中,Python 解释器按以下方式执行:
- 设置 GIL
- 切换到一个线程去运行
- 运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0))
- 把线程设置为睡眠状态
- 解锁 GIL
- 再次重复以上所有步骤。
创建线程
直接创建线程对象:
from threading import Thread
import time
def task(name, delay):
print(f"{name} 开始执行")
time.sleep(delay)
print(f"{name} 执行完毕")
if __name__ == "__main__":
# 通过 Thread 类实例化,指定目标函数(target)和参数(args/kwargs)
t1 = Thread(target=task, args=("线程A", 2))
t1.start() # 启动线程
t1.join() # 等待线程结束
继承 Thread 类:
from threading import Thread
import time
# 通过子类化 Thread 并重写 run() 方法
class MyThread(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f"{self.name} 运行中...")
time.sleep(1)
if __name__ == "__main__":
t1 = MyThread("自定义线程")
t1.start()
多线程
示例代码:多线程运行
import threading
import time
import os
def task(name, delay):
print(f"当前线程 ID (Python标识符): {threading.get_ident()}")
print(f"线程对象标识符: {threading.current_thread().ident}")
print(f"{name}-{os.getpid()} 开始执行")
time.sleep(delay)
print(f"{name}-{os.getpid()} 执行完毕")
if __name__ == "__main__":
threads = [threading.Thread(target=task, args=(f"线程{i}", 2)) for i in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print("主线程/主进程pid", os.getpid())
常用方法
方法 | 含义 |
---|---|
Thread.isAlive() |
Thread 类中的对象方法:返回线程是否活动的 |
Thread.getName() |
返回线程名 |
Thread.setName() |
设置线程名 |
threading.currentThread() |
返回当前的线程变量 |
threading.enumerate() |
返回一个包含正在运行的线程的列表 |
threading.activeCount() |
返回正在运行的线程数量 |
示例代码:
from threading import Thread
import threading
from multiprocessing import Process
import os
def work():
import time
time.sleep(3)
print(threading.current_thread().getName())
if __name__ == "__main__":
t = Thread(target=work)
t.start()
print(t.is_alive())
print(threading.current_thread().getName())
print(threading.current_thread())
print(threading.enumerate())
print(threading.active_count())
t.join()
print("主线程/主进程")
print(t.is_alive())
守护线程
在 Python 中,守护线程(Daemon Thread) 是一种特殊的线程,它的生命周期与主线程(或程序的主进程)绑定。当所有非守护线程(即普通线程)结束时,无论守护线程是否完成任务,它都会被强制终止。这种机制常用于执行后台支持任务(如日志记录、心跳检测等),无需等待其完成。
核心特性:
- 依赖主线程存活:主线程结束时,守护线程立即终止(即使任务未完成)。
- 后台服务:通常用于非关键性任务,即使意外终止也不会影响程序逻辑。
- 资源释放风险:守护线程被终止时,可能不会正常释放资源(如文件句柄、网络连接),需谨慎使用。
示例代码:
import threading
import time
def background_task():
while True:
print("守护线程运行中...")
time.sleep(1)
# 创建线程并设置为守护线程
daemon_thread = threading.Thread(target=background_task)
daemon_thread.daemon = True
daemon_thread.start()
# 主线程执行其他操作
time.sleep(3)
print("主线程结束,守护线程将被终止")
线程同步机制
互斥锁
保证同一时刻只有一个线程能访问共享资源,防止数据竞争。
代码示例:
import threading
import time
def increment():
global shared_counter
with lock: # 自动获取和释放锁:lock.acquire() 和 lock.release()
tmp = shared_counter + 1
time.sleep(0.1)
shared_counter = tmp
if __name__ == "__main__":
shared_counter = 0
lock = threading.Lock()
# 启动多个线程修改共享变量
threads = [threading.Thread(target=increment) for _ in range(100)]
for t in threads:
t.start()
for t in threads:
t.join()
print(shared_counter) # 输出 100(无竞争)
死锁与可重入锁
死锁:两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
示例代码:
from threading import Lock as Lock
import time
mutexA = Lock()
mutexA.acquire()
mutexA.acquire() # 上面已经拿过一次key了,这边就拿不到了,程序被阻塞到这里
print(123)
mutexA.release()
mutexA.release()
可重入锁:threading.RLock
允许同一线程多次获取锁(避免死锁)。RLock 内部维护着一个 Lock和一个 counter 变量,counter 记录了 acquire 的次数,从而使得资源可以被多次 acquire。直到一个线程所有的 acquire 都被 release,其他的线程才能获得资源。
from threading import RLock as Lock
import time
mutexA=Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()
同步锁
- 协调线程间的执行顺序(如生产者-消费者模型)。
- 控制并发数量(如限制同时访问数据库的连接数)。
信号量
控制同时访问资源的线程数量:适用于限制并发数
import threading
semaphore = threading.Semaphore(3) # 最多允许3个线程同时运行
def task():
with semaphore:
print(f"{threading.current_thread().name} 正在工作")
# 模拟耗时操作
threading.Event().wait(3)
# 启动10个线程,但最多3个并发执行
threads = [threading.Thread(target=task) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
条件变量
实现线程间通知机制:适用于生产者-消费者模型
import threading
queue = []
condition = threading.Condition()
def producer():
with condition:
queue.append("EaglesLab")
condition.notify() # 通知等待的消费者
def consumer():
with condition:
while not queue:
condition.wait() # 等待生产者通知
data = queue.pop()
print(f"消费数据: {data}")
# 启动生产者和消费者线程
threading.Thread(target=producer).start()
threading.Thread(target=consumer).start()
事件
简单线程间状态通知:事件常用于跨线程的状态同步。
import threading
event = threading.Event()
def waiter():
print("等待事件触发...")
event.wait() # 阻塞直到事件被设置
print("事件已触发!")
def setter():
threading.Event().wait(2)
event.set() # 设置事件
threading.Thread(target=waiter).start()
threading.Thread(target=setter).start()
线程池
线程池通过预创建并复用一组线程,减少频繁创建/销毁线程的开销,适用于 I/O 密集型任务(如网络请求、文件读写)
- 优点:资源复用、负载均衡、简化线程管理。
- 适用场景:批量下载、Web 服务器请求处理、数据库并发查询。
基本操作
通过 concurrent.futures.ThreadPoolExecutor
实现
from concurrent.futures import ThreadPoolExecutor
def task(n):
return n * n
# 创建线程池(推荐使用 with 上下文管理)
with ThreadPoolExecutor(max_workers=5) as executor:
# 提交任务方式1:submit 逐个提交
future = executor.submit(task, 5)
print(future.result()) # 输出 25
# 提交任务方式2:map 批量提交
results = executor.map(task, [1, 2, 3])
print(list(results))
注意事项
- 线程数量:建议设为 CPU 核心数 × 2(I/O 密集型)
- 异常处理:通过
try-except
捕获future.result()
的异常 - 资源释放:使用
shutdown()
或上下文管理器自动关闭线程池
同步机制结合
当多个线程访问共享资源(如全局变量、文件)时,需通过同步机制避免资源竞争和数据不一致。
代码示例:
from concurrent.futures import ThreadPoolExecutor
from threading import Lock
def task():
global counter
with lock: # 使用锁保护共享变量
counter += 1
if __name__ == "__main__":
counter = 0
lock = Lock()
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(task) for _ in range(100)]
for future in futures:
future.result()
print(f"最终计数:{counter}")
案例解析
案例1
基于条件变量同步机制,实现多线程-生产/消费者模型完整版本
import threading
def producer(i):
with condition:
queue.append(f"EaglesLab {i}")
condition.notify() # 通知等待的消费者
def consumer(i):
with condition:
# 等待直到队列不为空或生产结束
while not queue and not producer_done:
condition.wait() # 等待生产者通知
if queue:
data = queue.pop()
elif producer_done:
return
print(f"消费者-{i} 消费数据: {data}")
if __name__ == "__main__":
queue = []
condition = threading.Condition() # 初始化条件变量
producer_done = False
# 启动生产者和消费者线程
pt = [threading.Thread(target=producer, args=(i,)) for i in range(3)]
ct = [threading.Thread(target=consumer, args=(i,)) for i in range(10)]
for t in pt + ct:
t.start()
with condition:
producer_done = True
condition.notify_all()
for t in pt:
t.join()
for t in ct:
t.join()
print("Main Process/Thread Done...")
课后作业
- [必须] 动手完成本章节案例
- [扩展] 阅读官方文档相关章节
- [扩展] 用多线程实现进程章节的爬虫案例