多线程基础回顾:进程与线程的区别、GIL锁机制、线程的创建与启动方式

各位同学好,我是你们的老朋友。今天咱们聊聊多线程的基础。说实话,这部分内容看似简单,但很多人在实际项目中栽跟头,往往就是基础没打牢。我见过不少五年经验的工程师,聊起GIL还是一脸懵。嗯,咱们从头捋一遍。

一、进程与线程:到底差在哪?

先问个问题:你打开一个浏览器,又打开一个IDE,这叫啥?这叫两个进程。那你在浏览器里开十个标签页呢?这通常是多个线程在干活。

说白了,进程是资源分配的最小单位,线程是CPU调度的最小单位。我习惯这么记:进程是房子,线程是房子里干活的人。房子(进程)有自己的地基、水电、煤气——也就是独立的内存空间。而线程呢?共享这个房子里的资源,但每个人有自己的私人物品——也就是线程私有的栈空间。

核心区别一览:

对比维度 进程 线程
资源开销 大(独立地址空间) 小(共享地址空间)
切换成本 高(需要切换页表) 低(只需切换栈和寄存器)
通信方式 IPC(管道、消息队列等) 直接读写共享变量
健壮性 一个进程挂了不影响其他 一个线程崩溃可能导致整个进程挂掉

我在项目中遇到过这么个事:有个同事用多进程做高并发服务,结果内存直接爆了。为啥?每个进程都加载了一份相同的模型数据。后来改成多线程,共享一份数据,内存占用直接降了70%。你想想看,选对模型有多重要。

二、Python的GIL锁:绕不开的坎

聊Python多线程,GIL是躲不开的话题。GIL全称是Global Interpreter Lock,全局解释器锁。它的存在让很多新手抓狂——说好的多线程并行呢?怎么变成了并发?

GIL的本质是什么? 它保证同一时刻只有一个线程在执行Python字节码。为什么要有这玩意儿?因为Python的内存管理不是线程安全的。CPython解释器内部大量使用引用计数,如果没有GIL,多个线程同时修改引用计数,内存就乱套了。

我的经验: 我曾经在一个爬虫项目里,用多线程抓取数据,结果发现CPU利用率只有30%。排查了半天,发现是GIL在作祟。后来换成多进程 + asyncio的组合,CPU直接跑满。所以记住:CPU密集型任务,多线程在Python里是坑;I/O密集型任务,多线程依然好用。

为什么会这样?因为I/O操作会释放GIL。当线程在等待网络响应或磁盘读写时,GIL会被释放,其他线程就能趁机执行。所以Python多线程在I/O密集场景下,效果还是不错的。

避坑指南: 我曾经天真地以为用C扩展就能绕过GIL。确实,C扩展可以手动释放GIL,但前提是你得确保C代码里没有操作Python对象。否则,轻则数据错乱,重则段错误。嗯,这里要注意,别踩坑。

三、线程的创建与启动方式

好了,理论说完了,咱们上手写代码。Python里创建线程的方式,我总结下来就三种主流玩法。

方式一:直接使用Thread类

import threading
import time

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

# 创建线程
t1 = threading.Thread(target=worker, args=("A",))
t2 = threading.Thread(target=worker, args=("B",))

# 启动线程
t1.start()
t2.start()

# 等待线程结束
t1.join()
t2.join()

print("所有线程执行完毕")

这种方式最直接,适合简单的任务。我习惯把耗时操作封装成函数,然后扔给线程去跑。

方式二:继承Thread类

class MyThread(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name
    
    def run(self):
        print(f"线程 {self.name} 开始工作")
        time.sleep(2)
        print(f"线程 {self.name} 工作结束")

t1 = MyThread("A")
t2 = MyThread("B")
t1.start()
t2.start()
t1.join()
t2.join()

这种方式适合需要封装复杂逻辑的场景。你可以在类里加各种属性、方法,代码组织更清晰。我个人习惯在需要管理线程状态时用这种方式。

方式三:使用线程池

from concurrent.futures import ThreadPoolExecutor

def worker(name):
    print(f"线程 {name} 开始工作")
    time.sleep(2)
    return f"线程 {name} 完成"

# 创建线程池,最多同时运行3个线程
with ThreadPoolExecutor(max_workers=3) as executor:
    futures = [executor.submit(worker, f"Task-{i}") for i in range(5)]
    for future in futures:
        print(future.result())

线程池是我最推荐的方式。为啥?因为频繁创建和销毁线程是有开销的。线程池复用线程,性能更好。我在做Web服务时,几乎都用线程池来管理并发请求。

三种方式对比:

方式 适用场景 优点 缺点
直接使用Thread 简单任务,少量线程 代码直观 线程管理麻烦
继承Thread类 需要封装状态和逻辑 面向对象,可扩展 代码稍多
线程池 大量任务,高并发 性能好,资源可控 需要理解池化概念

四、知识体系总览

说了这么多,咱们用一张图把今天的内容串起来。我画了个结构图,帮你理清思路。

多线程基础回顾 - 知识体系 多线程基础 进程 vs 线程 独立内存空间 共享内存空间 GIL 全局解释器锁 CPU密集:性能差 I/O密集:性能好 线程创建方式 直接Thread类 继承Thread类 线程池 核心:理解GIL的局限,选择合适的并发模型

这张图把今天的内容串起来了。左边是进程与线程的区别,中间是GIL这个绕不开的坎,右边是三种创建线程的方式。你想想看,掌握了这些,后面讲性能分析和内存优化,你才能跟得上。

最后说一句: 多线程不是银弹。我见过太多人一上来就开几十个线程,结果性能反而下降。记住:合适的才是最好的。先搞清楚你的任务是CPU密集还是I/O密集,再决定用多线程、多进程还是协程。

好了,基础就聊到这儿。代码示例我都跑过,放心用。有问题咱们随时交流。

可可精品资料库