【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
背景
上篇 blog
【Ubuntu】【Gitlab】拉出内网 Web 服务:http.server 单/多线程分析(六)
提到了并发和并行是两个概念,其中并发是多个任务交替推进,而并行是多个任务物理上同时进行,并分析了 GIL 锁限制 Python 字节码并行执行的原因,下面继续
Python http.server 单/多线程分析
上篇 blog 分析了 CPython 通过一把全局大锁 GIL,限定了同一时刻只能有一个线程能持有 GIL
OK,那么现在又产生了新的问题,如果给每个对象单独加锁,那么是不是就没问题了?这种方案理论上可以,但
- 内存开销 :如果每个对象都要带一个 mutex 锁,首先内存开销就会爆炸(Python 对象本身就轻量但不紧凑)
- 性能原因 :除了内存开销外,加解锁不是免费的,即使是无竞争的 mutex,在现代 CPU 上也有几十纳秒的开销,而 Python 是解释型语言,操作频繁 ,频繁地加解锁也会导致性能严重下降(即使单线程也会如此),保守估计性能要下降 10% ~ 30%
- 死锁风险 :细粒度锁需要精心设计锁顺序,否则极易发生死锁现象(两个或多个线程互相等待对方释放资源资源锁,结果最后所有涉及的线程都无法继续执行,程序卡死) ,而 Python 的动态特性(比如任意对象可以作为字典
key,任意属性可以被修改),使得静态分析锁依赖几乎不可能,而且异常处理和垃圾回收机制等也会因为锁机制会变得极其复杂,造成死锁风险增加 - 历史与设计哲学 :Python 诞生于上世纪 90 年代,当时多核 CPU 尚未普及,而第一款消费级双核 CPU 是 2005 年的 Pentium D,此外,CPython 的核心原则之一就是简单,清晰,高效(对单线程而言),因此引入了 GIL,用一把粗粒度的大锁保护整个解释器状态,牺牲多线程并行,换取单线程性能和实现简单性
可以看到,GIL 锁之所以限制 Python 字节码的并行执行,根本原因是为了保护 CPython 内部数据结构的线程安全,尤其是引用计数机制(上篇 blog 介绍的)
另外,上面提到,Python 是解释型语言,操作频繁,这里通过对比 C 语言(编译型语言),再详细展开分析下
首先,在 C 语言中,list.append(x) 这样的高级操作不存在,用户必须自己管理数组,内存和边界等,比如下面有一个循环在频繁写入数组
c
for (int i = 0; i < N; i++) {
arr[i] = i;
}
- 首先,这里会编译成几条高效的机器指令(比如 MOV,ADD,CMP 等)
- 另外,这里没有函数调用,没有动态类型检查,也没有内存分配(如果预分配了)
- 如果用户真的要加锁(比如多线程写共享数组),通常只在临界区外加一次锁,而不是每次赋值都加锁,比如
c
enter_critic_area()
for (int i = 0; i < N; i++) {
arr[i] = i;
}
exit_critic_area()
即使不是循环操作,也要加临界区进行保护
c
int g_var = 0; # 全局变量
enter_critic_area()
g_var = 5;
exit_critic_area()
可以看到,C 语言的操作是底层的,更细粒度的,由程序员显式控制的,即便是最简单的赋值操作,也不是线程安全的,用户需要显式处理所有并发问题,没有魔法保护
对于 Python 而言,比如 lst.append(x) 这样的高级操作看似简单,但在 CPython 解释器内部会触发
- 查找
lst对象(名字解析) - 加载
.append方法(属性查找) - 调用
.append方法(函数调用开销) - 检查参数类型
- 可能触发内存
realloc再分配(如果列表满了的话) - 更新列表长度
- 增加新元素的引用计数
等一系列操作,这些操作都发生在这一次高级操作中,而这个操作在字节码层面就是一条 LIST_APPEND 或 CALL_METHOD 指令,可以看到,Python 的操作是高层的,更粗粒度的,隐式包含了大量运行时逻辑的,相当于对 C 语言的多个操作进行了一次封装
最后总结一下
- 从语义粒度(程序员视角) :对 C 语言而言,
arr[i] = i就是一个简单赋值 ,其背后不涉及对象创建,类型检查,内存管理等高层抽象逻辑 , 而 Python 中的list.append(x)则是一个高级操作 ,封装了扩容,引用计数,类型安全,内存分配等复杂逻辑。 - 从执行粒度(CPU,指令视角) :C 语言的粒度更细 ,一条 C 赋值语句只有几条机器指令 (
mov,add等),每条语句都可以被精确控制 ,而一条 Python 字节码(比如LIST_APPEND)则可能包含数十,甚至上百条 C 指令,里面有函数调用,条件分支,内存操作等多种内容
所以,Python 的操作在语义上更小,但运行时开销更大 ,也就是上面说的,操作更频繁
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog