全局锁,锁住怎么办???

简介: 全局锁,锁住怎么办???

Python 的全局锁问题


问题核心:★★  

口感:麻油沾馍


问题


你已经听说过全局解释器锁 GIL,担心它会影响到多线程程序的执行性能。


解决方案


尽管 Python 完全支持多线程编程,但是解释器的 C 语言实现部分在完全并行执行时并不是线程安全的。

实际上,解释器被一个全局解释器锁保护着,它确保任何时候 都只有一个 Python 线程执行。

GIL 最大的问题就是 Python 的多线程程序并不能利用 多核 CPU 的优势(比如一个使用了多个线程的计算密集型程序只会在一个单 CPU 上 面运行)。

在讨论普通的 GIL 之前,有一点要强调的是 GIL 只会影响到那些严重依赖 CPU 的程序(比如计算型的)。

如果你的程序大部分只会涉及到 I/O,比如网络交互,那么 使用多线程就很合适,因为它们大部分时间都在等待。

实际上,你完全可以放心的创建 几千个 Python 线程,现代操作系统运行这么多线程没有任何压力,没啥可担心的。

而对于依赖 CPU 的程序,你需要弄清楚执行计算的特点。

例如,优化底层算法 要比使用多线程运行快得多。类似的,由于 Python 是解释执行的,如果你将那些性能 瓶颈代码移到一个 C 语言扩展模块中,速度也会提升的很快。

如果你要操作数组,那 么使用 NumPy 这样的扩展会非常的高效。

还有一点要注意的是,线程不是专门用来优化性能的。一个 CPU 依赖型程序可能 会使用线程来管理一个图形用户界面、一个网络连接或其他服务。这时候,GIL 会产生 一些问题,因为如果一个线程长期持有 GIL 的话会导致其他非 CPU 型线程一直等待。

事实上,一个写的不好的 C 语言扩展会导致这个问题更加严重,尽管代码的计算部分 会比之前运行的更快些。

说了这么多,现在想说的是我们有两种策略来解决 GIL 的缺点

首先,如果你完 全工作于 Python 环境中,你可以使用 multiprocessing 模块来创建一个进程池,并像协同处理器一样的使用。

例如,你有如下的线程代码:


#执行大型计算(CPU限制)
def some_work(args):
...
    return result
# 调用上述函数的线程
def some_thread():
    while True:
      ...
      r = some_work(args)
      ...

修改代码,使用进程池:

    pool = None
    #执行大型计算(CPU限制)
    def some_work(args):
        return result
    # 调用上述函数的线程
    def some_thread():
      while True:
          r = pool.apply(some_work, (args))
    # 线程池
    if __name__ == '__main__':
      import multiprocessing
      pool = multiprocessing.Pool()

    这个通过使用一个技巧利用进程池解决了 GIL 的问题。

    当一个线程想要执行 CPU 密集型工作时,会将任务发给进程池。然后进程池会在另外一个进程中启动一个单独的 Python 解释器来工作。当线程等待结果的时候会释放 GIL。

    并且,由于计算任务在单 独解释器中执行,那么就不会受限于 GIL 了。在一个多核系统上面,你会发现这个技术可以让你很好的利用多 CPU 的优势。

    另外一个解决 GIL 的策略是使用 C 扩展编程技术。

    主要思想是将计算密集型任务转移给 C,跟 Python 独立,在工作的时候在 C 代码中释放 GIL。

    这可以通过在 C 代码中插入下面这串代码来完成:


    #include "Python.h"
    ...
    PyObject *pyfunc(PyObject *self, PyObject *args) {
    ...
    Py_BEGIN_ALLOW_THREADS
    ...
    Py_END_ALLOW_THREADS
    ...
    }


    结论


    作为一个真实的例子,在多线程的网络编程中神秘 的 stalls 可能是因为其他原因比如一个 DNS 查找延时,而跟 GIL 毫无关系。

    最后你需要先去搞懂你的代码是否真的被 GIL 影响到。同时还要明白 GIL 大部分都应该 只关注 CPU 的处理而不是 I/O.如果你准备使用一个处理器池,注意的是这样做涉及到数据序列化和在不同 Python 解释器通信。被执行的操作需要放在一个通过 def 语句定义的 Python 函数中, 不能是 lambda、闭包可调用实例等,并且函数参数和返回值必须要兼容 pickle。C 扩展最重要的特征是它们和 Python 解释器是保持独立的。也就是说,如果你准 备将 Python 中的任务分配到 C 中去执行,你需要确保 C 代码的操作跟 Python 保持独立,这就意味着不要使用 Python 数据结构以及不要调用 Python 的 C API。也就是说 C 扩展担负起 了大量的计算任务,而不是少数几个计算。


    -END-

    ﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌﹌

    往期好文合集

    怎么还蹦出来个 “ 数据管道 ”<<戳这里

    Python 读取 JSON 数据的骚操作<<戳这里一切皆对象,那是我的对象<<戳这里


    最 后   


    若觉得文章不错,转发分享,也是我们继续更新的动力。在公众号内回复「资料」,即可免费获取 Python资料,期待你的关注~

    相关文章
    |
    6月前
    |
    算法 C语言 C++
    C++ std::chrono库使用指南 (实现C++ 获取日期,时间戳,计时等功能)(一)
    C++ std::chrono库使用指南 (实现C++ 获取日期,时间戳,计时等功能)
    2296 1
    |
    人工智能 Linux Python
    代码自动补全工具——Kite安装教程(以Pycharm为例)及Failed to install PyCharm at path.Plugin configuration directory的解决方案
    代码自动补全工具——Kite安装教程(以Pycharm为例)及Failed to install PyCharm at path.Plugin configuration directory的解决方案
    代码自动补全工具——Kite安装教程(以Pycharm为例)及Failed to install PyCharm at path.Plugin configuration directory的解决方案
    |
    2月前
    |
    JSON 安全 编译器
    扩展类实例的序列化和反序列化
    扩展类实例的序列化和反序列化
    35 0
    |
    6月前
    |
    存储 安全 算法
    【C++ 17 包裹类 泛型容器 std::any】深入理解与应用C++ std::any:从泛型编程到多态设计
    【C++ 17 包裹类 泛型容器 std::any】深入理解与应用C++ std::any:从泛型编程到多态设计
    460 1
    |
    存储 关系型数据库 MySQL
    从一个案例深入剖析InnoDB隐式锁和可见性判断(4)
    从一个案例深入剖析InnoDB隐式锁和可见性判断
    691 0
    |
    NoSQL 关系型数据库 索引
    从一个案例深入剖析InnoDB隐式锁和可见性判断(1)
    从一个案例深入剖析InnoDB隐式锁和可见性判断
    从一个案例深入剖析InnoDB隐式锁和可见性判断(1)
    |
    SQL 关系型数据库 MySQL
    从一个案例深入剖析InnoDB隐式锁和可见性判断(2)
    从一个案例深入剖析InnoDB隐式锁和可见性判断
    146 0
    从一个案例深入剖析InnoDB隐式锁和可见性判断(2)
    |
    缓存 Linux 开发工具
    【Git】创建本地仓库 | 连接远程仓库
    【Git】创建本地仓库 | 连接远程仓库
    337 1
    【Git】创建本地仓库 | 连接远程仓库
    |
    5月前
    |
    算法 程序员 存储
    时间复杂度与空间复杂度详解
    时间复杂度与空间复杂度详解
    |
    关系型数据库 MySQL 索引
    从一个案例深入剖析InnoDB隐式锁和可见性判断(3)
    从一个案例深入剖析InnoDB隐式锁和可见性判断
    117 0