在并发访问过程当中保持数据一致性是一个很广泛的问题,而使用锁则是该问题一个很广泛的解决方法。全局解释器锁(global interpreter lock)如其名运行在解释器主循环中,在多线程环境下,任何一条线程想要执行代码的时候,都必须获取(acquire)到这个锁,运行必定数量字节码,而后释放(release)掉,而后再尝试获取。这样 GIL 就保证了同时只有一条线程在执行。python
通常来讲,GIL 并不会带来麻烦,由于大多数程序的性能瓶颈都在 IO 上(IO-bound)。但当你运行计算密集型代码,并且计算量又很是大的时候,好比数据分析,你就会发现问题了——你的程序对 CPU 的使用率很是低。以我如今使用的 i5-460m 双核四线程(2C/4T)处理器来讲,Python 最多只能占用 25% 的 CPU 负载,正好是一条线程的量。不管是多核心,仍是 intel 的超线程技术,如今提高 CPU 性能的有效手段已经从提升单核性能转变为增长核心数量,即执行并行(parallelism)运算。显然 GIL 的存在与这种思路有些格格不入。编程
若是参考其余实现的话,你可能会问一个问题,为何要使用全局锁,而不是一个更细粒度的锁呢?实际上 Linux 的文件系统就是这样作的,进程给目标文件加锁的时候,能够只加必定字节数的锁,只要另外一个进程准备加的锁与其没有交集的话,这两个锁就能够共存,这两个进程也能够同时修改这一个文件(的不一样部分)。所以对于 Python,也许能够给对象加锁(一切皆对象嘛),同时不限制线程的并行执行。但从网上的信息来看,彷佛这种思路曾经被尝试实现过,但细粒度的锁会给单线程模式下的性能形成明显影响。因此仍是用 GIL 吧~网络
一般,咱们不会责备某我的笨,但要是他笨还不努力的话可能就得说道说道了。Python 如今的状况就是,使用这门语言的人基本都已经接受它不如 C 快这一点了,可是对于那些用着 4 核 8 线程,甚至 6 核 12 线程的 CPU 的人来讲,看着本身的程序在 10% CPU 负载下执行两个小时确实是一件很别扭的事。数据结构
intel 的超线程,是使一个物理核心能够在极短期内“同时”执行两条或多条线程的技术,它在操做系统上的体现为:一个物理核心会被当作两个逻辑核心使用。这即是当你打开任务管理器,并切换到【性能】标签页后,会看到比你 CPU 核心数量多一倍的小方格的缘由。多线程
更加具体的物理实现这里就不说了,由于我也不懂。下面只经过一些实验数据来看一下超线程 CPU 在不一样线程下的效率表现。禁用 HT 的操做必须在 BIOS 中进行,很惋惜笔记本的 BIOS 是简化过的,没有这个选项,因此我用本身电脑进行的测试都是运行在 HT enable 的状态,禁用后的数据来自网络。并发
以双核四线程的 i5-460m 为例,在 Windows 上表现为 4 个逻辑核心,分别名为 “CPU 0”,“CPU 1”,“CPU 2” 和 “CPU 3”。其中 0 和 1 是同一个物理核心,2 和 3 是另外一个物理核心。在任务管理器的进程标签页中能够为指定进程设置处理器相关性,即为其分配指定的逻辑内核。或者也能够经过 cmd 命令:start /affinity <mask> .exe
来在程序打开前指定处理器相关性,其中 <mask>
应被替换为 CPU 编号的 16 进制掩码。如,为名为 test.py
的脚本分配 CPU 0 和 CPU 2 并执行的命令为:start /affinity 5 python test.py
(2^0 ^+ 2^2 ^= 5)。对于如象棋或 CINEBENCH 这样的测试软件,直接在任务管理器中设置便可,出于执行时间考虑,本文使用国际象棋来进行测试。 <br /> ###测试数据(HT 启用) 单核单线程: <br />函数
<table style="font-size:14px"> <tr> <th>CPU 0</th> <th>CPU 1</th> <th>CPU 2</th> <th>CPU 3</th> </tr> <tr> <td>1645</td> <td>1627</td> <td>1726</td> <td>1704</td> </tr> </table> <br /> **双核单线程:** <br /> <table style="font-size:14px"> <tr> <th>CPU 0+1</th> <th>CPU 1+2</th> <th>CPU 2+3</th> </tr> <tr> <td>1654</td> <td>1743</td> <td>1735</td> </tr> </table> <br /> 双核心跑单线程时,CPU 负载会分布在两个核心中。这时若是把资源监视器里面的 CPU 视图截图下来,并把两个核心中的一个(上图)作垂直旋转,而后以 50% 透明度叠加到另外一个核心的视图(下图)上,就能看到他们基本是彻底互补的。0+1 和 1+2 有少许重叠的阴影存在,缘由不是很肯定,多是其余进程的负载吧。这样两个核心的负载总和只有单逻辑核心的 100%,即 i5-460m 的 25% 。性能
四核单线程:测试
1747优化
双核双线程: <br />
<table style="font-size:14px"> <tr> <th>CPU 0+1</th> <th>CPU 0+2</th> <th>CPU 0+3</th> <th>CPU 1+2</th> <th>CPU 1+3</th> <th>CPU 2+3</th> </tr> <tr> <td>2282</td> <td>3537</td> <td>3496</td> <td>3494</td> <td>3526</td> <td>2375</td> </tr> </table> <br /> **四核四线程:**
4472 <br /> ###测试数据(HT 禁用) HT 禁用模式下的数据来源于网络,由于与个人环境不一样,这里直接给结论吧:
<br /> ###结论 上面数据中的看点,或者说比较出乎我意料的地方在于:
想要不改动任何代码地实现性能提高看来是不可行了。其实对于 GIL 的问题,包括官方给出的最多见的解决方案是:使用多进程,multiprocessing 模块。
上一节说到本身写的多进程程序未必比单线程的第三方包快,一方面的缘由在于,计算密集型的扩展包不少都是用 C/C++ 这样的编译型语言写的。而这类扩展包有一个很重要的特性在于,它能够在被调用时选择释放 GIL,从而在不影响主程序的状况下独立运算。
但问题在于 C 代码很差写,更别说须要使用第三方数据结构的状况了。这时候在 C 扩展和 Python 之间的一种折中方案就是 Cython,对于 Cython 能够简单地将其当作是拥有静态类型并能嵌入 C 函数的 Python。Cython 也一样支持释放 GIL 后的并行。
不管是多进程,仍是编写扩展,在实行前都得计算一下成本收益,依据能够节省的时间的量来决定付出多少的努力。对于多数偶发性的状况,可能优化一下本身的代码就能带来很好的效率提高,又没必要为此花掉太多时间。所以对于 multiprocessing 和 Cython 的实现细节,这里就先不深究了。(好水的文)