文章目录
Python中进程和线程的区别
| 方面 | 多进程(multiprocessing) | 多线程(threading) |
|---|---|---|
| 定义 | 在同一个进程中运行的多个线程 | 操作系统中独立运行的多个进程 |
| 内存空间 | 多个线程共享进程内存 | 每个进程有独立的内存空间 |
| 创建开销 | 小,线程比进程轻量 | 大,创建进程比线程开销大 |
| 切换开销 | 低,线程切换快 | 高,进程切换慢 |
| 共享数据 | 同一进程的线程共享内存,线程间可以通过全局变量交换数据 | 不共享内存,需要通过Pipe。Queue等传递数据 |
| 线程安全 | 需要使用锁来放置数据竞争,进行线程同步 | 每个进程独立,天然安全,但通信需要序列化和 IPC 开销 |
说说Python中GIL锁
GIL(全局解释器锁)是Cpython解释器中内部的一个互斥锁,它保证同一时刻在同一个进程中,任意时刻只有一个线程在执行 Python 字节码。
GIL锁的作用:
- 保证对象模型在多线程环境下的安全,例如python使用
引用计数标记对象是否可回收,多线程同时修改可能会导致计数错误从而可能发生内存泄漏或者程序崩溃。 - 通过保证只有一个线程执行,也可以简化python的内部实现,执行很多对象之前无需额外的锁。
GIL锁的特点:
- 单线程执行:同一个时刻只能有一个线程执行字节码,所以即使在多核CPU环境下也无法同时运行多个线程的Python代码。
- 释放条件:在 I/O 操作(如文件、网络、数据库)时,线程会释放 GIL,允许其他线程运行。
- 影响:在
I/O密集型任务中,多线程仍然可以提高执行效率。但是在CPU密集型任务中,多线程仍然无法真正并行,效果可能不如单线程执行。
由于GIL锁的机制,当你想利用多核CPU做计算,可以选择
多进程或者使用例如C/C++拓展等机制绕过GIL锁机制。
Python中的线程同步
Lock
threading.Lock 本质互斥访问共享资源,特点:
- 同一时刻
只允许一个线程进入临界区 - 其它线程会被
阻塞,阻塞的线程会被OS挂起,不会进行空转。 - 不可重入,同一线程
拿到锁 - 再次尝试获取锁, 会造成死锁
常用使用形式:
python
import threading
lock = threading.Lock()
count = 0
def worker():
global count
for _ in range(100000):
with lock: # 等同于acquire + release
count += 1
适用场景
- 修改共享变量
- 操作共享容器
- 计数器、状态机修改。
RLock
threading.RLock可重入锁互斥锁,特点:
- 同一时刻
只允许一个线程进入临界区 - 同一线程可以多次获取同一把锁,因为内部维护
owner(拥有锁线程ID),count(重入次数)可以保证一个线程可以多次安全获取一个锁。
常见使用形式
python
lock = threading.RLock()
def f1():
with lock:
f2()
def f2():
with lock:
print("safe")
适用场景:需要多次访问同一资源的场景。
Semaphore(信号量)
threading.Semaphore(n)信号量,特点:
- 初始化函数
threading.Semaphore(n)可以指定最多n个线程可以同时访问资源。 - 本质上是一个计数器。当一个线程尝试获取时
n > 0 => n-=1, n == 0 =>线程阻塞
常见使用形式
python
# 单个线程可以同时访问
sem = threading.Semaphore(3)
def worker():
with sem:
print("running")
适用场景:
- 连接池资源限制
- 爬虫并发控制速率
Condition
threading.Condition条件变量用于实现线程间复杂同步,其核心方法包含:
wait(): 释放锁并等待通知notify(): 唤醒一个等待线程notify_all(): 唤醒所有等待线程
常用使用形式(生产者-消费者模型):
python
import threading
condition = threading.Condition()
buffer = []
def consumer():
while True:
with condition:
condition.wait() # 等待数据
if buffer:
data = buffer.pop()
print(f"Consumed: {data}")
def producer():
for i in range(3):
with condition:
buffer.append(i)
print(f"Produced: {i}")
condition.notify() # 通知消费者
import time
time.sleep(0.5)
threading.Thread(target=consumer, daemon=True).start()
producer()
适用场景:
- 生产者-消费者模型实现
- 任务调度
Event
threading.Event 事件,本质是一个布尔标志位
- 初始情况默认为
False set(),设置为Trueclear(),设置为Falsewait():等待事件变为True,进入阻塞状态
常见适用形式
python
import threading
import time
start_event = threading.Event()
def worker(name):
print(f"{name}:等待开始信号")
start_event.wait() # 阻塞在这里
print(f"{name}:收到信号,开始工作")
time.sleep(1)
print(f"{name}:工作完成")
threads = []
# 创建并启动线程
for i in range(3):
t = threading.Thread(target=worker, args=(f"线程{i}",))
t.start()
threads.append(t)
time.sleep(2) # 主线程做准备工作
print("主线程:准备完成,发出开始信号")
start_event.set() # 一次唤醒所有等待线程
for t in threads:
t.join()
print("主线程:所有线程结束")
适用场景:
- 传递启动/停止信号
- 广播通知
- 线程生命周期控制
Python中内存管理
Python 内存管理是Python程序性能优化和稳定运行的重要组成部分。内存管理能够确保程序在运行过程中有效地利用系统资源,防止不必要的内存消耗,避免内存泄露,并确保不再使用的对象能被及时释放,从而腾出内存供其他对象使用。
Python 的内存管理主要包括对象的分配、垃圾回收以及内存池机制。其中内存回收主要依赖于引用计数、标记-清除和分带回收三种策略实现自动内存管理,详细说说这几种机制
引用计数
python中一切皆对象,每个对象都有一个引用计数,每当新的引用指向该对象时,引用计数加1;当不再有引用指向该对象时,引用计数减1。当引用计数为0时,对象占用的内存就会认为可以被释放。
python
# python中对象包含的基本结构
typedef struct _object {
Py_ssize_t ob_refcnt; // 引用计数
PyTypeObject *ob_type; // 类型信息
} PyObject;
只使用引用计数会存在循环引用的情况,造成对象的引用计数永远不会变为0,从而造成泄漏。
python
# 会造成循环引用的情况
a = list()
b = list()
a.append(b)
b.append(a)
在这种情况下,尽管a和b在逻辑上已经不再需要,但由于彼此互相引用,引用计数不会归零,因此常规的引用计数方法无法回收它们占用的内存。
标记清除
python采用"标记-清除"算法来解决容器对象可能产生的循环引用问题。只有容器对象才会产生循环引用的情况,比如列表、字典、用户自定义对象等。像数字、字符串这类简单类型不会出现循环引用。
标记-清除算法包含两步:
- 标记阶段:该阶段会遍历所有的对象,如果是可达的,即还有对象引用它,那么就标记该对象为可达。
- 清除阶段:再次遍历对象,如果发现某个对象没有标记可达,则将其回收。
python会使用一个双向链表讲这些容器组织起来,构成一个有向图。其中对象构成有向图的节点,而引用关系构成有向图的边。系统会从rootOject(全局变量、调用栈、寄存器,这些不会被删除对象)出发,沿着有向边遍历对象,可达的对象标记为活动对象,不可达的对象就是要被清除的非活动对象。
这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。
分代收集
分代回收是建立在标记清除技术基础之上的,是一种以空间换时间的操作方式。
Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3"代",分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。
- 新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,
- 依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。