python 多线程伪_一文读懂python多线程

python 多线程伪_一文读懂python多线程_第1张图片

讲解 python 多线程的文章有很多,但很多都解释的不清不楚,甚至有的文章还抛出 python 是伪多线程的观点。那 python 到底有没有多线程的能力呢?

python 中存在着全局解释锁(GIL),这也是很多文章重复了很多遍。GIL 限制了 python 同一时间只能有一条线程在跑。如果是这样,那些计算密集型的项目,比如 Opencv, TensorFlow 又是怎么利用 python 做并行计算呢?这里我抛出了两个问题:

1、python 能不能使用多线程?

2、python 能不能使用多核并发?

对于问题1,我的回答是能。 python 支持多线程,它的 threading 模块底层就是用 pthread 实现的(不信可以去看源码),所以 threading 起的线程就是实打实的线程。虽然可以通过 threading 起多个线程,但是由于 GIL 的存在,同一时间只能有一条线程在执行,其他线程都在阻塞。这里就有个前提条件,这些线程必须是 python 线程,如果不是则不受 GIL 的控制。那什么是 python 线程呢?简单来说被 GIL 锁住的线程,就是 python 线程,当某一线程释放了 GIL,那么它就不再是 python 线程,想怎么执行就怎么执行。

那 python 线程和非 python 线程有什么区别呢?区别在于 python 线程可以调用 python 的 API,比如 Py_INCREF, PyArg_ParseTupleAndKeywords 等开头为 Py 的接口函数。而非 python 线程不能调用 python 的 API,但好处是不受 GIL 的影响。

python 和非 python线程之间是可以自由切换的,python 线程调用 PyEval_SaveThread 就可以切换为非 python 线程,相反地调用 PyEval_RestoreThread 就切换回 python 线程。在浏览 python 关于 GIL 的 API 时,你可能还会注意到另外一对函数 PyGILState_Ensure 和 PyGILState_Release,这两个函数又是干嘛的呢?简单来讲,当线程不知道自己是 python 还是非 python 线程,但又要调用 python 的 API 时,就需要 ensure 一下,获取 GIL,确保自己是 python 线程,调用完后再用 PyGILState_Release 释放掉。

对于问题2,python 是能使用多核并发执行,由以一个问题的答案可知,把需要并行执行的任务用非 python 线程执行就可以了。

下面我们用一段代码来验证以上观点。

// test_extend.cc
#include 
#include "Python.h"

using namespace std;

class PyAllowThreads
{
      
public:
  PyAllowThreads() : _state(PyEval_SaveThread()) {}
  ~PyAllowThreads()
  {
      
      PyEval_RestoreThread(_state);
  }
private:
  PyThreadState* _state;
};

vector< vector > matrix_multiply(vector< vector > A, vector< vector > B) {
      
    int row_a = A.size();
    int col_a = A[0].size();

    int row_b = B.size();
    int col_b = B[0].size();

    vector< vector > res;
    if (col_a != row_b) {
      
        return res;
    }

    res.resize(row_a);
    for (int i = 0; i < row_a; i++) {
      
        res[i].resize(col_b);
    }

    for (int i = 0; i < row_a; i++) {
      
        for (int j = 0; j < col_b; j++) {
      
            for (int k = 0; k < col_a; k++) {
      
                res[i][j] += A[i][k] * B[k][j];
            }
        }
    }

    return res;
}

static PyObject * long_running_test(PyObject *self, PyObject *args, PyObject *kw) {
      
    int dim;
    int M, K, N;
    M = K = N = 1;
    const char* keywords[] = {
      "dimension", NULL};
    if( PyArg_ParseTupleAndKeywords(args, kw, "i:Extend.long_running_test",
        (char**)keywords, &dim)) {
      
        // 释放GIL,切换到非 python 线程
        // 可以这行代码屏蔽,再观察 cpu 的占有率   
        PyAllowThreads allowThreads;

        M = K = N = dim;
        printf("running (%d, %d) * (%d, %d)n", M, K, K, N);
        
        // 初始化 A, B 矩阵
        vector< vector > A;
        vector< vector > B;
        A.resize(M);
        for (int i = 0; i < M; i++) {
      
            A[i].resize(K);
            for (int j = 0; j < K; j++) {
      
                A[i][j] = 1;
            }
        }
        B.resize(K);
        for (int i = 0; i < K; i++) {
      
            B[i].resize(N);
            for (int j = 0; j < N; j++) {
      
                B[i][j] = 1;
            }
        }

        // 计算矩阵相乘
        vector< vector > C = matrix_multiply(A, B);
    }
    Py_RETURN_NONE;
}

static PyMethodDef ExtendMethods[] = {
      
    {
      "long_running_test", (PyCFunction)long_running_test, METH_VARARGS | METH_KEYWORDS},
    {
      NULL, NULL}
};

static struct PyModuleDef extendmodule = {
      
    PyModuleDef_HEAD_INIT,
    "Extend",
    NULL,
    -1,
    ExtendMethods
};

PyMODINIT_FUNC
PyInit_Extend(void){
      
    PyObject *m;
    m = PyModule_Create(&extendmodule);
    if(m == NULL) {
      
        return NULL;
    }
    return m;
}

上面这段代码主要实现了两个 N*N 的矩阵的乘法,并提供了 python 接口。这是一段很笨的矩阵乘法代码,性能非常差,但是不要紧,我们的目的是把 CPU 跑满。

下面是CMake工程。

CMakeLists.txt

project(pyExtend)
cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
set(CMAKE_CXX_STANDARD 11)

find_package(Python COMPONENTS Interpreter Development)
execute_process(COMMAND ${
      Python_EXECUTABLE} -c "import distutils.sysconfig; print(distutils.sysconfig.get_config_var('EXT_SUFFIX'))"
                RESULT_VARIABLE PYTHON_CVPY_PROCESS
                OUTPUT_VARIABLE CVPY_SUFFIX
                OUTPUT_STRIP_TRAILING_WHITESPACE)
if(NOT PYTHON_CVPY_PROCESS EQUAL 0)
    set(CVPY_SUFFIX ".so")
endif()

include_directories(${
      Python_INCLUDE_DIRS})
set(SRC test_extend.cc)
add_library(Extend SHARED ${
      SRC})
target_link_libraries(Extend PUBLIC ${
      Python_LIBRARIES})
set_target_properties(Extend PROPERTIES
    PREFIX ""
    OUTPUT_NAME Extend
    SUFFIX ${
      CVPY_SUFFIX})

将 CMakeLists.txt 和c文件放在同一目录,执行 cmake . && make

得到一个名字为 Extend.cpython-37m-x86_64-linux-gnu.so 库文件。如果想在 python 里面引入这个库,只需要加入代码 import Extend 就可以了。

下面是测试代码:

(test.py)

import threading
import time
import Extend as et

DIM = 1000
def test():
    et.long_running_test(dimension=DIM)

# thread 1
t1 = threading.Thread(target=test)
t1.setDaemon(True)
t1.start()

# thread 2
t2 = threading.Thread(target=test)
t2.setDaemon(True)
t2.start()

# main thread
et.long_running_test(dimension=DIM)

当你跑起来,使用 htop 或者 top 查看进程的 cpu 占用率,在多核机器上(大于或等于3核),你会发现 cpu 的占有率为 300%,因为这里起了3条线程,2条子线程和1条主线程。所以 python 支持多核多线程。

当把 PyAllowThreads allowThreads 这行代码注释掉,无论起多少个线程 cpu 占有率永远为100%。

很多高性能计算库都使用了这个机制,numpy 就是其中之一,我们可以测试下面这段代码:

(test_numpy.py)

import threading
import numpy as np

DIM = 30000

def test():
    A = np.ones((DIM, DIM))
    B = np.ones((DIM, DIM))
    C = np.multiply(A, B)

# thread 1
t1 = threading.Thread(target=test)
t1.setDaemon(True)
t1.start()

# thread 2
t2 = threading.Thread(target=test)
t2.setDaemon(True)
t2.start()

# main thread
test()

CPU 占有率同样为300%。

结论:

1、python 支持多线程多核并行执行,只要正确地释放 GIL 就可以了。

2、在做计算密集型任务时,应该使用 numpy 等高性能计算库,它们会把计算都下放到非 python 线程,应避免使用 python 自带的加减乘除。

你可能感兴趣的:(python,多线程伪)