进程与线程:并发编程基础
摘要:进程与线程是操作系统面试的必考点,也是理解 AI 分布式训练和多线程数据加载的基础。本文从进程内存模型出发,系统讲解线程同步机制(锁、信号量、条件变量),并通过 Python 代码展示多线程爬虫和生产者-消费者模型的实现。
一、进程 vs 线程:核心区别
1.1 内存模型对比
图 1:进程内存模型
| 特性 | 进程 | 线程 |
|---|---|---|
| 地址空间 | 独立 | 共享(除栈外) |
| 切换开销 | 大(需切换页表) | 小 |
| 通信方式 | IPC(管道、共享内存、Socket) | 直接读写共享变量 |
| 崩溃影响 | 不影响其他进程 | 可能导致整个进程崩溃 |
| 创建开销 | 大 | 小 |
1.2 Python 的 GIL
Python 的GIL(全局解释器锁)意味着同一时刻只有一个线程执行 Python 字节码。但这不意味着多线程无用:
- IO 密集型任务:网络请求、文件读写,多线程仍有收益;
- CPU 密集型任务:应使用多进程(
multiprocessing)绕过 GIL。
二、线程同步机制
2.1 锁、信号量、条件变量
""" 线程同步机制演示 包含锁、信号量、条件变量 """importthreadingimporttimefromqueueimportQueuefromtypingimportListclassThreadSyncDemo:"""线程同步演示"""@staticmethoddefdemo_lock():""" 互斥锁演示 保护共享计数器的线程安全 """counter=0lock=threading.Lock()defincrement():nonlocalcounterfor_inrange(100000):withlock:# 获取锁counter+=1threads=[threading.Thread(target=increment)for_inrange(5)]fortinthreads:t.start()fortinthreads:t.join()print(f"带锁计数器结果:{counter}(期望 500000)")@staticmethoddefdemo_semaphore():""" 信号量演示 限制同时访问资源的线程数量 """semaphore=threading.Semaphore(3)# 最多 3 个线程同时执行defworker(worker_id:int):withsemaphore:print(f"Worker{worker_id}开始执行")time.sleep(1)print(f"Worker{worker_id}执行完毕")threads=[threading.Thread(target=worker,args=(i,))foriinrange(6)]fortinthreads:t.start()fortinthreads:t.join()@staticmethoddefproducer_consumer():""" 生产者-消费者模型 使用条件变量实现线程间协调 """queue=Queue(maxsize=10)defproducer():foriinrange(20):item=f"item_{i}"queue.put(item)print(f"生产:{item}")time.sleep(0.1)defconsumer():for_inrange(20):item=queue.get()print(f"消费:{item}")queue.task_done()time.sleep(0.2)p=threading.Thread(target=producer)c=threading.Thread(target=consumer)p.start()c.start()p.join()c.join()deftest_thread_sync():print("="*60)print("线程同步测试")print("="*60)demo=ThreadSyncDemo()print("\n【测试 1】互斥锁")demo.demo_lock()print("\n【测试 2】信号量")demo.demo_semaphore()print("\n【测试 3】生产者-消费者")demo.producer_consumer()print("\n"+"="*60)if__name__=="__main__":test_thread_sync()代码 1:线程同步机制
三、AI 场景:多进程数据加载
""" PyTorch DataLoader 风格的多进程数据加载 """frommultiprocessingimportPool,QueueasMPQueueimporttimedefpreprocess_data(item_id:int)->dict:"""模拟数据预处理"""time.sleep(0.1)# 模拟耗时操作return{"id":item_id,"features":[item_id*iforiinrange(10)]}defparallel_data_loading(num_items:int=100,num_workers:int=4):""" 并行数据加载 使用多进程绕过 GIL,加速 CPU 密集型预处理 """start=time.time()withPool(num_workers)aspool:results=pool.map(preprocess_data,range(num_items))elapsed=time.time()-startprint(f"加载{num_items}条数据,{num_workers}个 Worker")print(f"耗时:{elapsed:.2f}秒")print(f"吞吐量:{num_items/elapsed:.1f}items/s")returnresultsdeftest_data_loading():print("\n并行数据加载测试:")parallel_data_loading(100,4)if__name__=="__main__":test_data_loading()代码 2:多进程数据加载
四、面试高频问题
Q1:进程间通信有哪些方式?
答:管道、消息队列、共享内存、信号量、Socket、信号。
Q2:死锁的四个必要条件?
答:互斥、占有且等待、不可抢占、循环等待。破坏任一条件即可避免死锁。
Q3:Python 的多线程为什么不适合 CPU 密集型任务?
答:GIL 限制同一时刻只有一个线程执行 Python 字节码,多线程无法并行利用多核。
五、总结
核心要点
- 进程是资源分配单位,线程是调度单位;
- 线程同步是并发编程的核心难点:锁粒度要细,持有时间要短;
- Python 中 IO 密集用线程,CPU 密集用进程;
- 死锁预防优于检测:设计时避免循环等待。
本文基于 Coding Interview University 项目整理,专注进程与线程专题。
