Python多线程&&多进程&&协程

针对Python的多线程、多进程、协程等进行学习。

Python 多线程 多进程 协程

GIL——Global Interpreter Lock

如果要谈python的多线程、多进程、协程,那么必须要先说一下GIL(Global Interpreter Lock——全局解释器锁)。GIL主要是用来保护访问Python对象的互斥锁(当多个线程同时去请求同一个对象是如果没有进行锁的操作会导致数据异常)。许多时候说到GIL都是直接跟python做了绑定关系,容易造成GIL是python的语言特性,其实,GIL并不是python的语言特性,而是CPython解释器特性(根据名字也可以知道)。由于GIL的关系导致python在同一时刻只有一个线程在运行,这就是大家为什么说python的多线程“无用”的原因。

python解释器:

  • CPython:官方默认解释器,由C语言开发,有GIL
  • IPython:基于CPython开发的交互式解释器,只是增强了交互功能,执行功能与CPython完全一样
  • PyPy:JIT(Just-In-Time Compiler 即使编译器),对python代码进行动态编译,有GIL
  • Jython:Java平台的python解释器,依赖java,无GIL
  • IronPython:.Net平台的python解释器,依赖.Net平台,无GIL

Ps:解释器跟编译器大概的区别:解释器立即执行代码、编译器为执行准备源码。

GIL可以理解为线程运行许可证,只有当线程拿到GIL时,这个线程才会进入CPU进行运行。那么如果运行CPU密集运算型任务时,多线程没有正面效果(有可能反面效果)。

多核CPU多线程<单核CPU多线程

每个CPU在同一时间只能运行一个线程 并发、非并行

在python多线程中,每个线程执行方式:

  1. 获取GIL
  2. 执行代码直至sleep或者python虚拟机将其挂起
  3. 释放GIL

Python2.x

在python2.x中,GIL释放逻辑是当前线程遇到IO操作或者ticks计数为100。ticks可以理解为python专用做于GIL的计数器,解释器指令,每次释放后归零,可通过sys.setcheckinterval来调整。

Python3.x

在python3.x中,GIL释放逻辑不使用ticks进行计数,改用计时器,执行时间达到阀值(5毫秒),当前线程释放GIL。


当GIL释放后,多核CPU上的线程开始竞争GIL

单核模型

A线程获取GIL——A线程运行——A线程释放GIL——B线程获取GIL——B线程运行——B线程释放GIL···

多核模型

CPU0:

A线程获取GIL——A线程运行——A线程释放GIL————B线程获取GIL——————线程B释放GIL——线程C获取

CPU1:

—————————————————————唤醒线程Q——线程Q等待——线程Q切换成待调度——唤醒线程W——线程W等待

通过上述模型可知多核CPU多线程劣于单核CPU多线程(CPU密集型任务)。

但是在IO密集型任务时多核多线程优于单核CPU多线程,这是因为IO密集型任务主要的时间时消耗在等待中。

GIL历史背景

python首次出现时间为1990,那时候CPU厂商还在通过提高核心频率来提高计算机性能。但在2000年后遇到天花板,开始转向多核方向来提高计算机性能。所以在一开始设计解释器时,没有想到多核时代这么快就来了。

GIL删除?

删除GIL有一个关键的难点:许多库依赖GIL,默认线程安全,如果删除去除GIL,那么代价太大。

多进程

由于多进程每个进程都有一个GIL,某些情况下多进程比多线程好。多进程劣势:

  • 相比线程笨重
  • 切换耗时长
  • 多进程数量不推荐超过cpu核心数(一个进程一个GIL,一个GIL跑满一个CPU)

协程

协程是编译器级,进程、线程是操作系统级。进程、线程由OS调度,开发者无法精确的控制。协程实现的是非抢占式调度,即如何调度可由开发者控制。

协程优点

  • 轻量、成本小
  • 减少CPU开销
  • 减少同步加锁
  • 同步思维处理异步代码

缺点

  • 不能有阻塞操作
  • 需特别关注全局变量、对象引用的操作

协程是基于事件循环方案,

Python源码保护

python源码文件py格式,字节码文件pyc、pyo等类型,pyc、pyo这些由解释器所生成的字节码文件,然后解释器再执行字节码文件。因此,可以通过重构一下python解释器来对python源码进行保护。

Pyc

pyc文件由三部分组成

  • 最开始4个字节是一个Maigc int, 标识此pyc的版本信息, 不同的版本的 Magic 都在 Python/import.c 内定义

  • 接下来四个字节还是个int,是pyc产生的时间(TIMESTAMP, 1970.01.01到产生pyc时候的秒数)

  • 接下来是个序列化了的 PyCodeObject(此结构在 Include/code.h 内定义),序列化方法在 Python/marshal.c 内定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    /* Magic word to reject .pyc files generated by other Python versions.
    It should change for each incompatible change to the bytecode.
    The value of CR and LF is incorporated so if you ever read or write
    a .pyc file in text mode the magic number will be wrong; also, the
    Apple MPW compiler swaps their values, botching string constants.
    The magic numbers must be spaced apart atleast 2 values, as the
    -U interpeter flag will cause MAGIC+1 being used. They have been
    odd numbers for some time now.
    There were a variety of old schemes for setting the magic number.
    The current working scheme is to increment the previous value by
    10.
    Known values:
    Python 1.5: 20121
    Python 1.5.1: 20121
    Python 1.5.2: 20121
    Python 1.6: 50428
    Python 2.0: 50823
    Python 2.0.1: 50823
    Python 2.1: 60202
    Python 2.1.1: 60202
    Python 2.1.2: 60202
    Python 2.2: 60717
    Python 2.3a0: 62011
    Python 2.3a0: 62021
    Python 2.3a0: 62011 (!)
    Python 2.4a0: 62041
    Python 2.4a3: 62051
    Python 2.4b1: 62061
    Python 2.5a0: 62071
    Python 2.5a0: 62081 (ast-branch)
    Python 2.5a0: 62091 (with)
    Python 2.5a0: 62092 (changed WITH_CLEANUP opcode)
    Python 2.5b3: 62101 (fix wrong code: for x, in ...)
    Python 2.5b3: 62111 (fix wrong code: x += yield)
    Python 2.5c1: 62121 (fix wrong lnotab with for loops and
    storing constants that should have been removed)
    Python 2.5c2: 62131 (fix wrong code: for x, in ... in listcomp/genexp)
    Python 2.6a0: 62151 (peephole optimizations and STORE_MAP opcode)
    Python 2.6a1: 62161 (WITH_CLEANUP optimization)
    Python 2.7a0: 62171 (optimize list comprehensions/change LIST_APPEND)
    Python 2.7a0: 62181 (optimize conditional branches:
    introduce POP_JUMP_IF_FALSE and POP_JUMP_IF_TRUE)
    Python 2.7a0 62191 (introduce SETUP_WITH)
    Python 2.7a0 62201 (introduce BUILD_SET)
    Python 2.7a0 62211 (introduce MAP_ADD and SET_ADD)
    .
    */

PyCodeObject包含了python代码中所有内容:字符串、常量值以及字节码指令等等