V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
black11black
V2EX  ›  问与答

C 语言插件可以默认摆脱 Python 的 GIL 约束吗?为什么测试结果不是如此?

  •  
  •   black11black · 2020-12-08 14:21:53 +08:00 · 777 次点击
    这是一个创建于 1236 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如题,最近用 cyhton 写了个加速插件,给 python 中的某个模块加速了两百倍左右,效果十分满意。

    于是又给算法复杂度加了十倍,这次 c 也顶不住了,单个函数调用时间一分钟左右。

    于是想到了多线程,因为 python 进入 3.6 时代后异步逐渐完善,日常里使用 python 多线程的地方主要还是在 IO,通过异步 IO 复用解决得很开心,所以很久没有用过多线程模块了。

    按照我记忆中的只是,python 的 GIL 机制是,如果你调用 C 插件的话是会默认释放 GIL 锁的,也就是说应该是原生使用多线程,并且在每个线程中调用 c 插件的话,它应该是可以利用多核心的,于是写了个简单小测试,但是结果不太对。我最终的结果还是顺序执行的时间,并没有利用多核心加速,是哪里做错了吗,还是我记错了,带佬来说一下。

    简单 demo 代码如下:

    # timer.py
    import zhishu
    import time
    from threading import Thread
    
    thread_list = [Thread(target = zhishu , args = (150000 , )) for _ in range(8)]
    
    st_time = time.time()
    for _ in thread_list:
        _.start()
    for _ in thread_list:
        _.join()
    print(time.time() - st_time)
    

    这个简单 demo 创建了 8 个线程,每个线程调用一个名叫 zhishu 的由 cython 编写的函数,这个函数的作用是计算 n 以内质数数量,我本机上单次调用大概需要 2s,我有 8 颗核心,理论上我期望他在 2s 内完成全部计算,但是最终得到的结果却是 16s

    cython 代码: 一个简单的质数计算器

    # zhishu.pyx
    import cython
    
    def zhishu(n):
        cdef cython.int count = 0
        cdef cython.int i , j , nn
        nn = <cython.int> n 
        for i in range(2 , n):
            for j in range(2 , i):
                if (i%j == 0):
                    break
            else:
                count += 1
        return count
    

    感谢!

    第 1 条附言  ·  2020-12-08 17:22:22 +08:00

    贴个条,用ctypes确实吃到全核心了,还是cython的问题。

    关于cython如何解锁gil,不知道有没有大佬指点一下,with nogil会报错提示仍有python成分所以不能释放gil,但是我看来看去也没感觉哪里python了,不全是c么。

    目前还有一个方案是全面移植ctypes,略微麻烦一些,主要是要解决如何把python数据结构传入c++的问题。现有传入结构为一个二维表,在python中映射为list内含dict这种样子,大概这样:

    lst = [
        {
            'id':0,
            'name':'Tom',
            'age':18,
        },
        {
            'id':0,
            'name':'Lily',
            'age':20,
        },
        ...
    ]
    

    这种感觉的。 并不懂怎么转,百度看了看没找到什么有效信息。之前之所以用cython写是因为cython很方便地自动把这些转换完成了,我在写C的时候只需要思考C的问题,写python的时候只需要思考python,现在不让自动转就人麻之麻麻了

    第 2 条附言  ·  2020-12-08 18:13:04 +08:00
    再贴个条

    cython notil 报错的原因找到了,因为 cython 默认所有函数调用都需要经过 gil,即使是 cdef 或者 cpdef 的也一样。所以需要在 cdef 后面声明 notil,下面才能用 with notil 调用。我在所有函数结尾加上 nogil 以后已经成功并行了,虽然感觉写起来略麻烦
    5 条回复    2020-12-08 23:22:53 +08:00
    agegcn
        1
    agegcn  
       2020-12-08 14:42:19 +08:00
    cython 有 nogil 的选项。因为 cython 里也会有 python object,所以需要手动释放 gil 锁
    westoy
        2
    westoy  
       2020-12-08 14:42:38 +08:00
    最简单的方案就是最里面的 for j 的 range 用 cython.parallel.prange(..., nogil=True)替换掉, 能用多核, 不过效果也有限

    建议写个原生 c 方案, 然后套个函数入口, 把参数转成原生类型后套到 with nogil 里面
    black11black
        3
    black11black  
    OP
       2020-12-08 15:28:31 +08:00
    @westoy
    @agegcn 所以是 cython 的问题,用 ctypes 的话就可以直接释放 gil 了吗?我记得很久以前在做 gil 实验的时候用 c 就解决了,没有什么做了特殊处理的印象。印象中当初 ctypes 折腾 c++stl 容器搞了很长时间没搞明白,就放弃这条路线了。现在直接手动编译 dll 的问题也在于 ctypes 折腾 stl 容器,以及把 python 的 list 映射到 vector 之类的这类输入输出的问题,中间处理倒是完全可以用 c++重写。

    试了试 with nogil 编译不通过啊,不知道问题在哪里,没感觉有什么非 C 内容啊。
    jones2000
        4
    jones2000  
       2020-12-08 23:03:19 +08:00
    c 内部开线程或并行计算. 外部 py 只做简单的调度和数据准备工作.
    helloworld000
        5
    helloworld000  
       2020-12-08 23:22:53 +08:00
    既然速度真的很重要,建议用 c++重写这个 module,然后用 pybind11 来给其他 python modules 调用
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1095 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 18:21 · PVG 02:21 · LAX 11:21 · JFK 14:21
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.