Post on: 2024-9-7Last edited: 2025-1-11Words 00 min

type
status
date
slug
summary
tags
category
icon
password
😀
之前做密码题经常遇到各种需要爆破30+比特的情况,这种时候会考虑爆破的效率问题,自然会想到多进程和多线程,用cpp写的速度很快,用python写多线程则似乎没啥用,因此深入来探究一下这个问题。

多线程vs多进程

多进程是一种并行执行的机制,通过创建多个独立的进程来同时执行不同的任务。每个进程都有自己独立的地址空间内存文件描述符等资源,进程之间的资源互不干扰。进程是操作系统调度的基本单位。 多线程是指在一个进程中创建多个线程,每个线程都可以独立执行任务。线程是进程中的轻量级执行单位,所有线程共享同一进程的内存和资源。线程间的切换开销较小,通信较为简单,因为它们共享同一块内存空间。 理论上来说,多线程和多进程都可以通过利用计算机的多核心进行并行运算来提高计算效率。但是具体机制和不同编程语言的编译、解释过程有关,不能一概而论。先写个小案例测试一下,python的多进程和多线程效率。我的笔记本电脑是12核心,因此这里分别调用12个进程和12个线程来对比。
理论上来说,死循环的代码这样运行都会占满cpu,但实际上只有多进程的写法cpu占用率能达到100,多线程大概只有1/12。也就是说多线程只用到一个核心。那这是为什么呢。这和Python解释器中的GIL锁有关:
notion image
简而言之就是,为了保证Python解释器在程序运行的过程中不会产生错误,需要GIL锁来保证每次只有一个线程占用cpu。那么Python的多线程是不是完全鸡肋呢,其实也不是,可以看看下面这张图:
notion image
思考一下,当一个程序存在IO操作,需要等待(比如简单的sleep),那么多线程仍然是有用的。如果没有多线程,在等待的时候也不能切换为其他线程执行,这就导致了资源的浪费,如果有其他线程,那么就可以在第一个线程等待的时候运行。比如下面这个代码示例:
显然这里的双线程起到了作用,因为在第一个线程挂起的时候,第二个线程无缝衔接cpu了,而这里cpu非密集,在线程1等待的时候线程2就执行完了也一起等待,因此总共用时只比2秒多一点。这里把2线程改为更多,结果也是一样的,不需要再赘述。 如果把代码改的稍微多一些运算,cpu更密集一些,用时就会明显高于2s了,如下代码:
到这里我们也可以得出结论了,python的多线程在cpu非常密集的情况下(比如爆破密钥)使用用处不大,但在io密集型程序中效果显著,因此python常用来多线程实现爬虫,就是这个理。

Python如何获得真正高效的并行运算

一.如上所述,第一种方案可以用多进程,虽然多进程在进程的创建和切换等用的资源更多,但比起单进程执行程序还是会好很多的。
二.python3.12以后GIL锁是可选关闭的,没有GIL锁自然也不存在“假多线程”的问题了,但是3.12版本关闭GIL似乎还比较麻烦,需要自己去重新编译一下解释器,期待以后更新的版本吧。
三.使用Cython。
Cython 可以看成是 Python 的超集,代码文件使用 .pyx 后缀。它融合了 Python 的语法风格和 C 的静态类型,写好之后可以编译成库文件,这样在 Python 中可以像对待其他 Python 库一样,直接使用 import 导入 Cython 编写的库。 如果把需要大量cpu资源的代码部分用C语言来实现,那么程序不就快很多了吗。Cython就可以用来实现这一需求,它能兼顾开发效率和执行效率。也就是把Python和C的优点都用上了。 我们用计算斐波那契数列的程序来说明如何使用Cython,可以看到pyx代码和python类似,但是增加了对参数类型的指定,这些参数类型是C语言的参数类型。
接下来将 fib.pyx 打包为共享库,这个过程分为两步: 1.使用 Cython 编译器将 Cython 代码优化后编译成 C 代码 2.将 C 代码编译成共享库 这两步用一个setup.py实现:
这里运行的命令为python setup.py build_ext --inplace ,windows下会生成一个pyd文件作为共享库。然后直接把生成的fib import进来,就可以调用了。
我用的vscode在打包共享库的时候会提醒error: Microsoft Visual C++ 14.0 or greater is required. Get it with "Microsoft C++ Build Tools":https://visualstudio.microsoft.com/visual-cpp-build-tools/
这里就需要到Microsoft C++ 生成工具 - Visual Studio 去安装一下Microsoft C++ 生成工具,再启动vscode就行了。
notion image
通过对 Python、C、Cython 三种斐波那契数列计算实现的性能测试,得出C>Cython>Python这一结论。关于三者效率方面的具体细节,可以参考《Cython系列》1. Cython 是什么?为什么要有 Cython?为什么我们要用 Cython? - 古明地盆 - 博客园 (cnblogs.com)
Cython 不仅能将使用 Python 风格编写的代码优化编译成 C,还将解释器中控制 GIL 的接口暴露了出来,可以通过 nogil 函数属性和 with nogil 上下文管理器来使用。从而实现真正的多线程。 通过在定义函数时指定 nogil 属性声明这个函数可以在 GIL 释放的环境中运行:
使用 nogil 修饰的函数必须要通过 Cython 中的 cdef 或者 cpdef 关键字定义, cdef 表示定义的是一个 C 函数,必须显式定义函数的参数和返回值得类型。 cpdef 表示同时定义了一个 C 函数和一个 Python 函数。 GIL 是为了保护 Python 对象的内存管理设置的,因此在 GIL 释放之后不能和 Python 对象发生交互,所以使用 def 定义的函数不能被 nogil 修饰,在 nogil 修饰的函数中也不能声明 Python 对象。 使用 with nogil 可以创建一个没有 GIL 的上下文环境,在这个环境中可以执行上面定义的 nogil 函数,例如:
with nogil 必须在存在 GIL 的环境中调用,例如下面的代码会出错:
如果需要重新获得 GIL,可以使用 with gil
下面分别使用 Python 和释放 GIL 的 Cython 实现斐波那契数列的计算,做一下效率的对比。启动 12 个线程,分别计算500次 fib(20) 的值,然后用 timeit 统计时间。
纯Python:
Cython但有GIL锁:
fib_cython_test_cython.pyx
fib_cython_test_setup.py
fib_cython_test_cython.py
Cython去除GIL锁:
fib_no_gil.pyx
setup过程和test代码用上面的就行。
可以看到用Cython之后效率远超Python,而去除GIL锁之后效率也有小部分提升。这里的效果不够显著应该是因为我测试的case代码的cpu密集程度不够,我的cpu还是能轻松应对。如果跑运算量更大的代码,三者之间的差距会很明显…师傅们有兴趣的话可以再去试试。
 

一个非常失败的Cython案例

🗒️一个非常失败的Cython案例

彻彻底底的失败。


DeadsecCTF2024

🗒️DeadsecCTF2024

某不知名比赛。