第33课:Python|并发编程基础【多线程创建、生命周期与线程安全详解】

在这里插入图片描述

文章目录


📖 开篇导读

在之前的课程中,我们的程序都是单线程顺序执行的——从第一行代码到最后一行,逐条执行。这种方式简单明了,但效率有限。例如,当你需要同时下载多个文件、处理多个用户请求、或者一边播放动画一边接收用户输入时,单线程就无法胜任了。

并发编程就是让程序能够“同时”执行多个任务的技术。在Python中,实现并发的主要方式有:多线程(threading)、多进程(multiprocessing)、异步IO(asyncio)。本课我们将从多线程开始,学习线程的创建、生命周期管理、线程间同步,以及Python特有的**全局解释器锁(GIL)**对多线程的影响。

💡 工作场景

  • 爬虫程序:同时下载多个网页,大幅提升抓取速度。
  • GUI程序:主线程负责界面刷新,工作线程执行耗时任务,避免界面卡死。
  • Web服务器:每个用户请求分配一个线程处理(或协程)。
  • 后台任务:定时清理缓存、发送邮件等,与主业务逻辑并行。

本课将学习:

  • 线程的概念与进程的区别
  • threading模块创建线程的多种方式
  • 线程的生命周期(启动、阻塞、结束、守护线程)
  • 线程安全问题与同步机制(LockRLockConditionSemaphore
  • Python GIL对多线程的影响及适用场景

学完本课,你将能够编写多线程程序,处理常见的并发问题,并理解Python多线程的应用边界。


🎯 学习目标

目标编号具体掌握内容对应面试/工作价值
1️⃣理解进程与线程的区别,明确多线程适用场景面试基础
2️⃣掌握使用threading.Thread创建和启动线程编写简单多线程程序
3️⃣理解线程的生命周期startjoin、守护线程daemon控制线程执行流程
4️⃣掌握线程同步机制:LockRLockConditionSemaphore避免资源竞争和数据错乱
5️⃣理解线程安全的概念,识别常见的不安全情况写出可靠的并发代码
6️⃣了解GIL对计算密集型与IO密集型任务的影响合理选择并发模型

🔥 面试考点:“进程和线程的区别”“Python多线程为什么不能充分利用多核?”“LockRLock的区别?”“join的作用?”“守护线程是什么?”


📚 知识点理论精讲

一、进程与线程的概念

1.1 进程(Process)

进程是资源分配的最小单位。每个进程拥有独立的内存空间、文件句柄等资源。进程间相互隔离,通信需要特殊机制(如管道、队列)。

1.2 线程(Thread)

线程是程序执行的最小单位,一个进程可以包含多个线程。线程共享进程的内存空间(全局变量、堆),但每个线程拥有独立的栈和寄存器状态。

1.3 多线程的优势与挑战

优势

  • 共享内存,通信方便。
  • 创建和切换开销比进程小。
  • 适合IO密集型任务(网络请求、文件读写)。

挑战

  • 线程安全问题:多个线程同时修改共享数据可能导致数据错乱。
  • 竞争条件、死锁等问题。

二、Python多线程基础:threading模块

2.1 创建线程的两种方式

方式一:直接使用Thread类,传入目标函数
import threading
import time

def worker(name, delay):
    print(f"线程 {name} 开始")
    time.sleep(delay)
    print(f"线程 {name} 结束")

t = threading.Thread(target=worker, args=("A", 2))
t.start()
方式二:继承Thread类,重写run()方法
class MyThread(threading.Thread):
    def __init__(self, name, delay):
        super().__init__()
        self.name = name
        self.delay = delay
    
    def run(self):
        print(f"线程 {self.name} 开始")
        time.sleep(self.delay)
        print(f"线程 {self.name} 结束")

2.2 线程的生命周期

  • 创建t = threading.Thread(target=func),此时线程处于新建状态。
  • 就绪/运行:调用t.start()后,线程被调度执行。
  • 阻塞:线程因等待锁、IO、time.sleep等进入阻塞状态。
  • 结束run()方法执行完毕,线程终止。

2.3 join()方法

join()让主线程等待子线程执行完毕。

t = threading.Thread(target=worker, args=("A", 2))
t.start()
t.join()  # 主线程在此等待,直到t结束
print("主线程继续")

2.4 守护线程(Daemon Thread)

守护线程会在主线程结束时自动终止,不需要显式等待。常用于后台任务(如心跳、监控)。

t = threading.Thread(target=worker, args=("Daemon", 10))
t.daemon = True
t.start()
# 主线程退出时,守护线程会被强制终止

三、线程安全与同步机制

当多个线程同时访问共享资源(如全局变量、文件)时,可能产生竞争条件(Race Condition),导致数据不一致。

3.1 锁(Lock)

锁是最基本的同步机制,保证同一时刻只有一个线程能执行加锁区域的代码。

lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        lock.acquire()
        counter += 1
        lock.release()

也可以使用上下文管理器:

with lock:
    counter += 1

3.2 可重入锁(RLock)

RLock允许同一个线程多次acquire,必须调用相同次数的release。适合递归函数或需要多次获取锁的场景。

rlock = threading.RLock()

def recursive_func(n):
    with rlock:
        if n > 0:
            recursive_func(n-1)

3.3 条件变量(Condition)

Condition用于线程间更复杂的同步,例如“生产者-消费者”模式,一个线程等待某个条件满足,另一个线程满足后通知。

cv = threading.Condition()

# 消费者
with cv:
    while not items:
        cv.wait()   # 释放锁并等待
    item = items.pop()

# 生产者
with cv:
    items.append(item)
    cv.notify()   # 唤醒等待的线程

3.4 信号量(Semaphore)

Semaphore允许最多n个线程同时访问资源。常用于限制并发数量(如数据库连接池)。

sem = threading.Semaphore(3)

def limited_access():
    with sem:
        # 最多3个线程同时执行此处
        pass

3.5 事件(Event)

Event用于线程间简单的事件通知。一个线程等待事件,另一个线程设置事件。

event = threading.Event()

def waiter():
    print("等待事件")
    event.wait()
    print("事件发生")

def setter():
    time.sleep(2)
    event.set()

四、Python的GIL(全局解释器锁)

4.1 什么是GIL?

CPython解释器中有一个全局锁,任何线程在执行Python字节码之前必须获得GIL。这意味着同一时刻只有一个线程能执行Python代码(即使有多核CPU)。

4.2 GIL的影响

  • 计算密集型任务:多线程无法利用多核,甚至因为线程切换开销,可能比单线程还慢。应使用多进程(multiprocessing)或异步IO。
  • IO密集型任务:线程在等待IO时释放GIL,多线程可以显著提升性能。适合网络爬虫、文件读写、数据库操作等。

4.3 验证GIL影响

import threading, time

def count(n):
    while n > 0:
        n -= 1

# 单线程耗时
start = time.time()
count(100000000)
print(f"单线程: {time.time()-start}")

# 双线程各计算一半
t1 = threading.Thread(target=count, args=(50000000,))
t2 = threading.Thread(target=count, args=(50000000,))
start = time.time()
t1.start(); t2.start()
t1.join(); t2.join()
print(f"双线程: {time.time()-start}")

通常情况下,双线程可能比单线程还慢(由于GIL争用)。


💻 代码案例实操

案例1:基础多线程——并发下载模拟

"""
thread_basic.py
演示创建线程,模拟并发下载
"""

import threading
import time

def download_file(file_name, duration):
    print(f"开始下载 {file_name},预计 {duration} 秒")
    time.sleep(duration)
    print(f"{file_name} 下载完成")

files = [("a.pdf", 2), ("b.mp4", 3), ("c.jpg", 1)]

threads = []
for name, dur in files:
    t = threading.Thread(target=download_file, args=(name, dur))
    threads.append(t)
    t.start()

# 等待所有线程完成
for t in threads:
    t.join()

print("所有下载任务完成")

案例2:守护线程——后台监控

"""
daemon_thread.py
演示守护线程:主线程结束时自动终止后台线程
"""

import threading
import time

def background_task():
    while True:
        print("后台监控运行中...")
        time.sleep(1)

t = threading.Thread(target=background_task)
t.daemon = True   # 设置为守护线程
t.start()

print("主线程运行,5秒后退出")
time.sleep(5)
print("主线程结束,守护线程将自动终止")

案例3:竞争条件与锁

"""
race_condition.py
演示多线程不加锁导致的竞争条件,以及使用锁修复
"""

import threading

counter = 0
lock = threading.Lock()

def increment_unsafe():
    global counter
    for _ in range(100000):
        counter += 1

def increment_safe():
    global counter
    for _ in range(100000):
        with lock:
            counter += 1

# 无锁版本
counter = 0
threads = [threading.Thread(target=increment_unsafe) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(f"无锁结果: {counter} (预期 500000)")

# 有锁版本
counter = 0
threads = [threading.Thread(target=increment_safe) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(f"有锁结果: {counter} (正确)")

案例4:生产者-消费者模型(使用Condition)

"""
producer_consumer.py
使用Condition实现生产者消费者模式
"""

import threading
import time
import random

class Queue:
    def __init__(self, maxsize=5):
        self.items = []
        self.maxsize = maxsize
        self.cond = threading.Condition()
    
    def put(self, item):
        with self.cond:
            while len(self.items) >= self.maxsize:
                print("队列已满,生产者等待")
                self.cond.wait()
            self.items.append(item)
            print(f"生产: {item}, 队列长度: {len(self.items)}")
            self.cond.notify()
    
    def get(self):
        with self.cond:
            while not self.items:
                print("队列为空,消费者等待")
                self.cond.wait()
            item = self.items.pop(0)
            print(f"消费: {item}, 剩余: {len(self.items)}")
            self.cond.notify()
            return item

def producer(q, id):
    for i in range(5):
        item = f"Producer{id}-{i}"
        q.put(item)
        time.sleep(random.uniform(0.1, 0.5))

def consumer(q, id):
    for _ in range(10):
        item = q.get()
        time.sleep(random.uniform(0.2, 0.6))

q = Queue(3)
producers = [threading.Thread(target=producer, args=(q, i)) for i in range(2)]
consumers = [threading.Thread(target=consumer, args=(q, i)) for i in range(2)]

for t in producers + consumers:
    t.start()
for t in producers + consumers:
    t.join()

案例5:使用信号量限制并发数量

"""
semaphore_demo.py
限制同时访问某个资源的线程数量
"""

import threading
import time

sem = threading.Semaphore(3)

def access_resource(thread_id):
    with sem:
        print(f"线程 {thread_id} 获得资源")
        time.sleep(2)
        print(f"线程 {thread_id} 释放资源")

threads = [threading.Thread(target=access_resource, args=(i,)) for i in range(10)]
for t in threads:
    t.start()
for t in threads:
    t.join()

案例6:定时器线程(Timer)

"""
timer_demo.py
使用Timer在指定时间后执行函数
"""

import threading
import time

def delayed_greeting(name):
    print(f"Hello, {name}")

print("主线程: 启动定时器,3秒后执行")
timer = threading.Timer(3, delayed_greeting, args=("张三",))
timer.start()

print("主线程: 等待定时器")
timer.join()  # 可选等待
print("主线程结束")

案例7:线程局部数据(ThreadLocal)

"""
thread_local.py
使用threading.local为每个线程存储独立的数据
"""

import threading
import time

local_data = threading.local()

def worker(name):
    local_data.value = name
    time.sleep(0.1)
    print(f"线程 {name} 的值: {local_data.value}")

t1 = threading.Thread(target=worker, args=("A",))
t2 = threading.Thread(target=worker, args=("B",))
t1.start(); t2.start()
t1.join(); t2.join()

案例8:GIL对计算密集型任务的影响验证

"""
gil_impact.py
验证Python多线程在计算密集型任务上的低效
"""

import threading
import time

def countdown(n):
    while n > 0:
        n -= 1

def run_single():
    start = time.time()
    countdown(100000000)
    print(f"单线程耗时: {time.time() - start:.2f}s")

def run_multi():
    start = time.time()
    t1 = threading.Thread(target=countdown, args=(50000000,))
    t2 = threading.Thread(target=countdown, args=(50000000,))
    t1.start(); t2.start()
    t1.join(); t2.join()
    print(f"多线程耗时: {time.time() - start:.2f}s")

if __name__ == "__main__":
    run_single()
    run_multi()
    # 通常多线程更慢,因为GIL导致频繁切换

⚠️ 易错点避坑总结

序号坑点描述后果解决方案
1忘记join()导致主线程提前退出,子线程被强制终止子线程任务未完成在需要等待时调用join()
2在锁内执行IO操作或耗时操作降低并发性能锁保护的范围尽量小
3使用acquire()忘记release()导致死锁使用with lock:语句
4多个锁获取顺序不一致导致死锁程序永久阻塞确保所有线程获取锁的顺序一致
5错误地认为Lock可重入同一线程再次acquire会死锁需要重入时使用RLock
6使用time.sleep代替同步机制效率低下,不可靠使用正确的同步原语
7忽视GIL,在多核上期望多线程加速计算任务实际性能下降计算密集型用多进程
8共享可变对象(如列表、字典)未加锁数据损坏对该对象加锁或使用线程安全的数据结构
9在守护线程中访问主线程资源主线程关闭时守护线程可能访问已释放资源避免守护线程依赖主线程资源
10线程间通信使用轮询而非通知CPU占用高使用ConditionEvent

📝 课后实战练习题

第1题:多线程下载模拟

编写程序,使用多线程同时下载5个文件(用sleep模拟下载时间分别为1,2,3,4,5秒)。记录总耗时,并与单线程顺序下载对比。

第2题:使用锁保护银行账户

定义一个BankAccount类,有balance属性和depositwithdraw方法。创建两个线程同时向同一账户存款和取款,使用锁保证余额正确。

第3题:生产者-消费者队列

使用queue.Queue(线程安全)重写案例4的生产者消费者模型,比较与Condition实现的简洁性。

第4题:实现线程池

创建一个简单的线程池类ThreadPool,初始化时创建固定数量的工作线程,提供submit(task, *args, **kwargs)方法提交任务,并返回Future对象。提示:使用queue.Queue存放任务。

第5题:死锁复现与解决

编写两个线程,每个线程先获取lock_a再获取lock_b,另一个先获取lock_b再获取lock_a,造成死锁。然后改变获取顺序解决。

第6题:定时器实现倒计时

使用threading.Timer实现一个可中断的倒计时器。每个整秒打印剩余秒数,用户输入stop可以取消计时器。

第7题:多线程文件搜索

给定一个目录,使用多线程并行查找所有包含指定关键词的文本文件。每个线程负责处理一部分文件,将结果路径写入线程安全的列表。


🧠 知识点思维导图总结

第33课:多线程基础

进程与线程概念

进程:资源单位

线程:执行单位

多线程共享内存,切换轻量

创建线程

target, args

继承Thread并重写run

线程生命周期

start → 就绪/运行

join 等待结束

daemon 守护线程

线程同步

Lock(互斥锁)

RLock(可重入锁)

Condition(条件变量)

Semaphore(信号量)

Event(事件)

Queue(线程安全队列)

线程安全

竞争条件

不可变对象安全

使用锁保护共享可变状态

GIL

全局解释器锁

同一时刻仅一个线程执行Python字节码

影响:计算密集型变慢,IO密集型适用

面试考点

进程线程区别

GIL原因及影响

Lock与RLock

join与daemon


🔜 下节课预告

多线程由于GIL的限制,对于计算密集型任务并不适用。下一节课我们将学习多进程,它能够绕过GIL,充分利用多核CPU。

第34课:多进程、进程池、线程池原理与企业级实战用法

内容包括:

  • multiprocessing模块创建进程
  • 进程间通信(Queue、Pipe、共享内存)
  • 进程池Pool和线程池ThreadPoolExecutor
  • concurrent.futures高级接口
  • 实战:并行计算、文件批量处理

掌握多进程后,你将能够真正利用多核优势,处理CPU密集型任务。

🌟 学习鼓励:多线程乍看简单,但并发编程中的调试和推理比单线程复杂得多。请动手运行每个案例,特别是竞争条件和死锁的例子,并尝试修改参数观察现象。理解GIL对Python多线程的约束非常重要,它会指导你在合适的场景选择正确的并发模型。继续前进,下一课的多进程将更加强大!


🔗《50节课 Python 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值