代码改变世界

Python并发编程学习 day2

2025-06-16 16:47  第二个卿老师  阅读(39)  评论(0)    收藏  举报

Day 2:Python 多线程编程进阶

一、 基础使用

多线程适用于处理那些花费大量时间等待外部事件的任务,目前标准库中有基础的threading模块与高级的concurrent.futures模块,任务需要复杂控制用threading,简单任务用concurrent.futures

  • threading模块
    • 用法:通过threading.Thread创建并启动线程
    • 关键参数:target / name / args / kwargs
    • 常用方法:run() / start() / join()
    • 示例:
      t = threading.Thread(target=worker, args=(arg1, arg2))
      t.start()
      #t.join()  # 这会阻塞调用这个方法的线程,直到被调用 join() 的线程终结 
      

主线程默认为非守护线程,程序需要等待所有非守护线程运行完成后才会退出

  • concurrent.futures模块
    • 用法:可使用with语句来管理线程池的创建和销毁,通过ThreadPoolExecutor对象来异步执行
    • 关键参数:max_workers / thread_name_prefix
    • 常用方法:submit() / map()
    • 示例:
      executor = ThreadPoolExecutor(max_workers=1)
      a = executor.submit(worker)
      
      # 推荐通过上下文管理器来执行,使用.map()来遍历一个可迭代对象
      #with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
      #    executor.map(thread_function, range(3))
      

python3.8到3.13前版本 : max_workers 的默认值最低5个,最高32个,IO任务设置参考:处理器数 * 5

二、同步原语

多线程并发运行时,会由操作系统或python解析器执行上下文切换,所以程序的结果不可控,这时就会出现竞争条件(常见的是共享变量),python提供了多个同步原语来解决竞争条件,保证线程安全

线程安全是指算法或程序在多个线程同时执行时能够正确运行的特性,如果代码在多线程环境中运行时,能够确定性地运行并产生预期的输出,则该代码被视为线程安全的

  • Lock(互斥锁)
    • 用法:保护临界区,避免竞态,推荐使用上下文管理器来运行
    • 方法:acquire() / release()
    • 示例:
      lock = threading.Lock()
      with lock:
      	...
    

.acquire():当Lock对象状态未锁定时,.acquire()将Lock对象更改为锁定状态并立即返回。如果Lock对象处于锁定状态,.acquire()会阻塞其他线程的程序执行,并等待持有锁的线程释放Lock对象。
.release():当Lock对象状态被锁定时,来自其他线程的.acquire()方法调用将阻止它们的执行,直到持有锁的线程在Lock上调用.release()。它应该只在锁定状态下调用,因为它将状态更改为未锁

  • RLock(可重入锁)
    • 与普通锁区别:同一线程可多次 acquire(),避免死锁,有稍微的性能开销
    • 示例:
      lock = threading.RLock()
      with lock:
      	...
    

死锁的原因有两种:
1,嵌套锁获取:如果线程试图获取它已经持有的锁,就会发生死锁。在传统的锁中,试图在同一线程中多次获取相同的锁会导致线程阻塞,这种情况在没有外部干预的情况下无法解决
2,多锁获取:当使用多个锁,并且线程以不一致的顺序获取它们时,可能会出现死锁。如果两个线程各持有一个锁并等待另一个锁,则两个线程都不能继续,从而导致死锁

  • Event(事件)

    • 用法:线程间事件通知
    • 方法:set() / clear() / wait()
    • 示例:
      write = threading.Event()
      def worker(data):
      	writr.wait()
      	...
    
      people = [
      	{"name": "worker 1", "type": "1"},
      	{"name": "worker 2", "type": "2"},
      ]
    
      with ThreadPoolExecutor(max_workers=4) as executor:
      	for person in people:
      		executor.submit(worker, person)
      	...
      	write.set()
    
  • Condition(条件变量)

    • 用法:更复杂的线程同步,配合 Lock 使用
    • 方法:wait() / notify() / notify_all()
    • 示例:
      customer_available_condition = threading.Condition()
      customer_queue = []
      def serve_customers():
      	while True:
      		with customer_available_condition:
      			# 等待顾客到来
      			while not customer_queue:
      				customer_available_condition.wait()
      			# 服务顾客
      			customer = customer_queue.pop(0)
      def add_customer_to_queue(name):
      	with customer_available_condition:
      		customer_queue.append(name)
      		customer_available_condition.notify()
    
      customer_names = [
      	"Customer 1",
      	"Customer 2"
      ]
    
      with ThreadPoolExecutor(max_workers=6) as executor:
      	teller_thread = executor.submit(serve_customers)
      	for name in customer_names:
      		executor.submit(add_customer_to_queue, name)
    

Condition常用于生产者-消费者场景,选择 notify(n=1) 还是 notify_all() ,取决于一次状态改变是只能被一个还是能被多个等待线程所用

  • Semaphore(信号量)
    • 用法:使用计数器来限制多个线程对临界区的访问
    • 关键参数:value
    • 方法:acquire() /release()
    • 示例:
      teller_semaphore = threading.Semaphore(2)
      
      def serve_customer(name):
      	with teller_semaphore:
      		# 服务顾客
      		...
      customers = [
      	"Customer 1",
      	"Customer 2",
      	"Customer 3"
      ]
      with ThreadPoolExecutor(max_workers=5) as executor:
      	for customer_name in customers:
      		thread = executor.submit(serve_customer, customer_name)
    

信号量通常用于保护数量有限的资源,例如数据库服务器的连接数,在资源数量固定的任何情况下,都应该使用有界信号量(threading.BoundedSemaphore),在生成任何工作线程前,应该在主线程中初始化信号量

  • Barrier(栅栏)
    • 用法:允许一组线程相互等待后再继续执行,类似于性能测试的集合点
    • 关键参数:value / action / timeout
    • 方法:wait() / reset()
    • 示例:
      teller_barrier = threading.Barrier(3)
      def prepare_for_work(name):
      	# 等待所有人员准备就绪
      	teller_barrier.wait()
      	...
      tellers = ["Teller 1", "Teller 2", "Teller 3"]
    
      with ThreadPoolExecutor(max_workers=3) as executor:
      	for teller_name in tellers:
      		executor.submit(prepare_for_work, teller_name)
    

创建栅栏时最好提供一个合理的超时时间,来自动避免某个线程出错

如果程序涉及到了共享数据以及非原子操作,就会出现线程不安全问题,上述同步原语已经解决了大部分问题,下面说说线程安全队列。

三、线程安全队列

在常见的生产者-消费者模型中,引入线程安全队列,来高效处理多线程之间同步大量消息的场景

  • queue.Queue(线程安全队列)
    • 用法:内置锁、可设置 maxsize,支持阻塞或超时
    • 方法:qsize() / put() / get() / task_done() / join()
    • 示例:
      q = queue.Queue()
    
      def worker():
      	while True:
      		item = q.get()
      		print(f'Working on {item}')
      		print(f'Finished {item}')
      		q.task_done()
    
      # 启动工作线程。
      threading.Thread(target=worker, daemon=True).start()
    
      # 向工作线程发送三十个任务请求。
      for item in range(30):
      	q.put(item)
    
      # 阻塞直到所有任务完成。
      q.join()
      print('All work completed')
    

四、生产者–消费者模型实战

1. 场景描述

生产者-消费者问题也称为有界缓冲区问题,拿两个进程来说

  • 生产者:生产者是一个循环进程,每次循环都会产生一定量的信息,需要消费者进行处理
  • 消费者:消费者也是一个循环进程,每次循环都会处理生产者产生的下一部分信息

2. 核心代码结构

import threading
import queue
import time
import random

q = queue.Queue(maxsize=10)

def producer():
    for i in range(20):
        item = f"item-{i}"
        q.put(item)
        print(f"[生产] {item}")
        time.sleep(random.uniform(0.1, 0.5))

def consumer(name):
    while True:
        try:
            item = q.get(timeout=3)
            print(f"[{name} 消费] {item}")
            # 处理逻辑……
            q.task_done()
        except queue.Empty:
            break

if __name__ == "__main__":
    # 1. 创建并启动线程
    prod = threading.Thread(target=producer)
    cons = [
        threading.Thread(target=consumer, args=(f"消费者-{i}",))
        for i in range(3)
    ]

    # 2. 启动
    prod.start()
    for t in cons: t.start()

    # 3. 等待完成
    prod.join()
    for t in cons: t.join()
    print("Done")

🔁 今日思考题

  1. 在多线程中,Lock 与 RLock 的使用场景有何区别?
    答:Lock在不需要线程重新获取已持有的锁的任务中使用,而Rlock允许同一个线程多次获取锁而不会导致死锁,则在递归函数或线程需要重新进入已锁定的资源的情况下非常有用
  2. 如果生产者速度远超消费者,队列会发生什么?如何处理?
    答:队列会持续增大,直到把队列填满,这时消息存放队列会失败,消息可能会丢失,内存占用飙升,系统性能下降。
    根据场景选择策略有:1,丢弃新消息(直接忽略新产生的item);2,丢弃旧消息(使用deque(maxlen=capacity));3,等待重试(休眠后重试put操作);4,降级生产(降低生产者速率);5,持久化存储(将消息写入磁盘/数据库)
  3. 如何在生产者–消费者模型中优雅地优先关闭所有消费者?
    答:原则如下:1,先停止生产者:防止新任务持续加入;2,清空队列:确保所有已入队任务被处理;3,发送退出信号:通知消费者停止工作;4,等待消费者退出:确保所有消费者线程/进程安全退出;5,清理资源:释放队列和线程资源