📜  从 C 和Python中释放 GIL 和混合线程(1)

📅  最后修改于: 2023-12-03 15:36:12.992000             🧑  作者: Mango

从 C 和 Python 中释放 GIL 和混合线程

什么是GIL

全局解释器锁(Global Interpreter Lock, GIL)是 Python 解释器的一个特性,它限制了同一时刻只能有一个线程执行 Python 代码。这是由于 Python 解释器的内存管理机制使得 Python 对于多线程的支持不够友好,多线程在解释器上切换时需要耗费大量的资源,为了规避这些问题,Python 引入了 GIL。

释放 GIL

由于 GIL 的存在,Python 的多线程表现并不突出,但是在某些应用场景中,如 CPU-bound 任务,我们需要使用多线程来提高运行速度。那么如何才能摆脱 GIL 的限制呢? 解决方法是将耗时的操作放到 C 扩展中执行,提高计算速度的同时也释放了 GIL。

在使用 C 扩展时释放 GIL

在 Python 中,我们可以使用 ctypes 或者 cython 将部分计算任务从 Python 代码转移到 C 代码中执行,这样就可以避免 GIL 拘束,提高效率。以下是使用 ctypes 的示例代码:

import ctypes
import time

lib = ctypes.CDLL("./mylib.so")

lib.cpu_bound.restype = ctypes.c_int32
lib.cpu_bound.argtypes = (ctypes.c_int32,)

def main():
    start = time.time()
    lib.cpu_bound(50000000)
    end = time.time()
    print(end - start)

if __name__ == "__main__":
    main()

在 C 代码中,我们需要添加 Py_BEGIN_ALLOW_THREADS 和 Py_END_ALLOW_THREADS 来暂时释放 GIL:

#include <Python.h>

int cpu_bound(int n) {
    Py_BEGIN_ALLOW_THREADS
    int res = 0;
    for (int i = 0; i < n; i++) {
        res += i;
    }
    Py_END_ALLOW_THREADS

    return res;
}

static PyObject* cpu_bound_py(PyObject* self, PyObject* args) {
    int n;
    if (!PyArg_ParseTuple(args, "i", &n)) {
        return NULL;
    }
    int res = cpu_bound(n);
    return PyLong_FromLong(res);
}

static PyMethodDef methods[] = {
    {"cpu_bound", cpu_bound_py, METH_VARARGS, "CPU bound task"},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef mymodule = {
    PyModuleDef_HEAD_INIT,
    "mymodule", NULL, -1, methods
};

PyMODINIT_FUNC PyInit_mymodule(void) {
    return PyModule_Create(&mymodule);
}
在使用 Python 时释放 GIL

除了使用 C 扩展来释放 GIL 之外,Python 还提供了一些 API 来帮助程序员短暂地释放 GIL。这种方法在 I/O-bound 任务中表现得更为优秀。以下是使用 with concurrent.futures.ThreadPoolExecutor() 来并行执行 I/O-bound 任务的示例代码:

import requests
import concurrent.futures

def fetch_url(url):
    with concurrent.futures.ThreadPoolExecutor() as executor:
        future = executor.submit(requests.get, url)
        response = future.result()
        return response.status_code

def main():
    urls = ["http://httpbin.org/status/200",
            "http://httpbin.org/status/201",
            "http://httpbin.org/status/202",
            "http://httpbin.org/status/203",
            "http://httpbin.org/status/204",
            "http://httpbin.org/status/500",
            "http://httpbin.org/status/503",
            "http://httpbin.org/status/504",
            "http://httpbin.org/status/505"]
    start = time.time()
    with concurrent.futures.ThreadPoolExecutor() as executor:
        futures = [executor.submit(fetch_url, url) for url in urls]
        for future in concurrent.futures.as_completed(futures):
            try:
                result = future.result(timeout=5)
            except Exception as e:
                print(e)
                continue
            print(result)
    end = time.time()
    print(end - start)

if __name__ == "__main__":
    main()

同时,Python 还提供了一些原子操作和锁来帮助程序员绕过 GIL,可以更精细地控制程序的运行。

混合线程

混合线程指的是同时使用多线程和多进程资源来提高操作系统优化和性能。在运用混合线程的同时,要特别注意锁的使用,以防止资源的竞争和错误。

以下是使用 concurrent.futures 和 multiprocessing 来处理混合线程的示例代码:

import os
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def func(num):
    pid = os.getpid()
    tid = int.from_bytes(os.getrandom(2), "big")
    print(f"Process {pid}, thread {tid} processing task {num}")
    return num*num

def main():
    with ThreadPoolExecutor(max_workers=3) as executor1, ProcessPoolExecutor(max_workers=3) as executor2:
        tasks = [executor1.submit(func, i) for i in range(9)]
        results = [executor2.submit(func, task.result()) for task in tasks]
        for result in results:
            print(result.result())

if __name__ == "__main__":
    main()
结语

以上就是从 C 和 Python 中释放 GIL 和混合线程的介绍,我们可以了解到如何通过使用 C 扩展和 API 来释放 GIL ,以及如何运用混合线程优化资源。当然,在开发时需要视实际情况选择合适的技术手段,做到最优解。