使用 Cython 和外部多线程 C 库时的未定义行为
Undefined behavior when using Cython and external multithreaded C library
我是 Cython(以及 Python)的新手,当我尝试公开外部多线程库的 C-API 时,我试图了解我哪里做错了到 Python。为了说明我的问题,我将回顾一个假设的 MWE。
假设我有以下目录结构
.
├── app.py
├── c_mylib.pxd
├── cxx
│ ├── CMakeLists.txt
│ ├── include
│ │ └── mylib.h
│ └── src
│ └── reduce_cb.cpp
├── mylib.pyx
└── setup.py
这里cxx
包含外部多线程库如下(头文件和实现文件串联):
/* cxx/include/mylib.h */
#ifndef MYLIB_H_
#define MYLIB_H_
#ifdef __cplusplus
extern "C" {
#endif
typedef double (*func_t)(const double *, const double *, void *);
double reduce_cb(const double *, const double *, func_t, void *);
#ifdef __cplusplus
}
#endif
#endif
/* cxx/src/reduce_cb.cpp */
#include <iterator>
#include <mutex>
#include <thread>
#include <vector>
#include "mylib.h"
extern "C" {
double reduce_cb(const double *xb, const double *xe, func_t func, void *data) {
const auto d = std::distance(xb, xe);
const auto W = std::thread::hardware_concurrency();
const auto split = d / W;
const auto remain = d % W;
std::vector<std::thread> workers(W);
double res{0};
std::mutex lock;
const double *xb_w{xb};
const double *xe_w;
for (unsigned int widx = 0; widx < W; widx++) {
xe_w = widx < remain ? xb_w + split + 1 : xb_w + split;
workers[widx] = std::thread(
[&lock, &res, func, data](const double *xb, const double *xe) {
const double partial = func(xb, xe, data);
std::lock_guard<std::mutex> guard(lock);
res += partial;
},
xb_w, xe_w);
xb_w = xe_w;
}
for (auto &worker : workers)
worker.join();
return res;
}
}
附带的 cxx/CMakeLists.txt
文件如下:
cmake_minimum_required(VERSION 3.9)
project(dummy LANGUAGES CXX)
add_library(mylib
include/mylib.h
src/reduce_cb.cpp
)
target_compile_features(mylib
PRIVATE
cxx_std_11
)
target_include_directories(mylib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
set_target_properties(mylib
PROPERTIES PUBLIC_HEADER include/mylib.h
)
install(TARGETS mylib
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
PUBLIC_HEADER DESTINATION include
)
对应的Cython文件如下(此时定义和实现文件拼接):
# c_mylib.pxd
cdef extern from "include/mylib.h":
ctypedef double (*func_t)(const double *, const double *, void *)
double reduce_cb(const double *, const double *, func_t, void *)
# mylib.pyx
# cython: language_level = 3
cimport c_mylib
cdef double func(const double *xb, const double *xe, void *data):
cdef int d = (xe - xb)
func = <object>data
return func(<double[:d]>xb)
def reduce_cb(double [::1] arr not None, f):
cdef int d = arr.shape[0]
data = <void*>f
return c_mylib.reduce_cb(&arr[0], &arr[0] + d, func, data)
# setup.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
setup(
ext_modules=cythonize([
Extension("mylib", ["mylib.pyx"], libraries=["mylib"])
])
)
构建 C++ 库,构建 Cython 扩展模块并按照说明将其链接到 C++ 库,当我尝试 运行
时出现未定义的行为
import mylib
from numpy import array
def cb(x):
partial = 0
for idx in range(x.shape[0]):
partial += x[idx]
return partial
arr = array([val + 1 for val in range(100)], "d")
print("sum(arr): ", mylib.reduce_cb(arr, cb))
对于未定义的行为,我的意思是我得到了
SIGSEGV
(地址边界错误),
- "Fatal Python error: GC object already tracked" 和
SIGABRT
,或者,
- (很少)正确的结果。
我已经彻底检查了 Cython 的文档(我猜),并且我已经搜索了 SO 和 Google 来解决这个问题,但是我找不到解决这个问题的合适方法。
基本上,我想要一个 C 库,它 unaware of Python 并使用来自多个线程的回调函数,集成在 Python。这是可能吗?我尝试了 nogil
签名和 with gil:
块,如 Cython 的 documentation, but I got compilation errors. Moreover, gc
related functionality in Cython seems to be valid only for extension types 中所讨论的,这不适用于我的情况。
我被卡住了,我会很感激任何 hint/help。
当您在没有锁定的情况下使用 Python-objects/functionality 时会发生这种情况。您的关键部分不仅是求和,而且是对函数 func
的调用,即:
workers[widx] = std::thread(
[&lock, &res, func, data](const double *xb, const double *xe) {
std::lock_guard<std::mutex> guard(lock);
const double partial = func(xb, xe, data); // must be guarded
res += partial;
},
xb_w, xe_w);
这首先让并行化毫无意义,不是吗?可能,从 software-engineering 的角度来看,更好的守卫位置是在包装函数 func
中——但我将其放入 worker
中,因为这样可以看到更好的结果。
Python 使用引用计数进行内存管理 - 类似于 std::shared_ptr
。然而它并没有像shared_ptr那样细粒度地锁定,它只在更改引用计数器时才锁定,而是使用更粗略的锁——全局解释器锁。结果是,当一个人从 open-MP-thread 或其他未在 Python-interpreter 中注册的线程更改 python-object 的引用计数时,reference-counter 不是 protected/guarded 并且出现竞争条件。您正在观察的是此类竞争条件的可能结果。
GIL 使您的努力或多或少变得不可能:您需要锁定每个可能的调用 python 但比序列化对此功能的调用更重要!
我是 Cython(以及 Python)的新手,当我尝试公开外部多线程库的 C-API 时,我试图了解我哪里做错了到 Python。为了说明我的问题,我将回顾一个假设的 MWE。
假设我有以下目录结构
.
├── app.py
├── c_mylib.pxd
├── cxx
│ ├── CMakeLists.txt
│ ├── include
│ │ └── mylib.h
│ └── src
│ └── reduce_cb.cpp
├── mylib.pyx
└── setup.py
这里cxx
包含外部多线程库如下(头文件和实现文件串联):
/* cxx/include/mylib.h */
#ifndef MYLIB_H_
#define MYLIB_H_
#ifdef __cplusplus
extern "C" {
#endif
typedef double (*func_t)(const double *, const double *, void *);
double reduce_cb(const double *, const double *, func_t, void *);
#ifdef __cplusplus
}
#endif
#endif
/* cxx/src/reduce_cb.cpp */
#include <iterator>
#include <mutex>
#include <thread>
#include <vector>
#include "mylib.h"
extern "C" {
double reduce_cb(const double *xb, const double *xe, func_t func, void *data) {
const auto d = std::distance(xb, xe);
const auto W = std::thread::hardware_concurrency();
const auto split = d / W;
const auto remain = d % W;
std::vector<std::thread> workers(W);
double res{0};
std::mutex lock;
const double *xb_w{xb};
const double *xe_w;
for (unsigned int widx = 0; widx < W; widx++) {
xe_w = widx < remain ? xb_w + split + 1 : xb_w + split;
workers[widx] = std::thread(
[&lock, &res, func, data](const double *xb, const double *xe) {
const double partial = func(xb, xe, data);
std::lock_guard<std::mutex> guard(lock);
res += partial;
},
xb_w, xe_w);
xb_w = xe_w;
}
for (auto &worker : workers)
worker.join();
return res;
}
}
附带的 cxx/CMakeLists.txt
文件如下:
cmake_minimum_required(VERSION 3.9)
project(dummy LANGUAGES CXX)
add_library(mylib
include/mylib.h
src/reduce_cb.cpp
)
target_compile_features(mylib
PRIVATE
cxx_std_11
)
target_include_directories(mylib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
set_target_properties(mylib
PROPERTIES PUBLIC_HEADER include/mylib.h
)
install(TARGETS mylib
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
PUBLIC_HEADER DESTINATION include
)
对应的Cython文件如下(此时定义和实现文件拼接):
# c_mylib.pxd
cdef extern from "include/mylib.h":
ctypedef double (*func_t)(const double *, const double *, void *)
double reduce_cb(const double *, const double *, func_t, void *)
# mylib.pyx
# cython: language_level = 3
cimport c_mylib
cdef double func(const double *xb, const double *xe, void *data):
cdef int d = (xe - xb)
func = <object>data
return func(<double[:d]>xb)
def reduce_cb(double [::1] arr not None, f):
cdef int d = arr.shape[0]
data = <void*>f
return c_mylib.reduce_cb(&arr[0], &arr[0] + d, func, data)
# setup.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
setup(
ext_modules=cythonize([
Extension("mylib", ["mylib.pyx"], libraries=["mylib"])
])
)
构建 C++ 库,构建 Cython 扩展模块并按照说明将其链接到 C++ 库,当我尝试 运行
时出现未定义的行为import mylib
from numpy import array
def cb(x):
partial = 0
for idx in range(x.shape[0]):
partial += x[idx]
return partial
arr = array([val + 1 for val in range(100)], "d")
print("sum(arr): ", mylib.reduce_cb(arr, cb))
对于未定义的行为,我的意思是我得到了
SIGSEGV
(地址边界错误),- "Fatal Python error: GC object already tracked" 和
SIGABRT
,或者, - (很少)正确的结果。
我已经彻底检查了 Cython 的文档(我猜),并且我已经搜索了 SO 和 Google 来解决这个问题,但是我找不到解决这个问题的合适方法。
基本上,我想要一个 C 库,它 unaware of Python 并使用来自多个线程的回调函数,集成在 Python。这是可能吗?我尝试了 nogil
签名和 with gil:
块,如 Cython 的 documentation, but I got compilation errors. Moreover, gc
related functionality in Cython seems to be valid only for extension types 中所讨论的,这不适用于我的情况。
我被卡住了,我会很感激任何 hint/help。
当您在没有锁定的情况下使用 Python-objects/functionality 时会发生这种情况。您的关键部分不仅是求和,而且是对函数 func
的调用,即:
workers[widx] = std::thread(
[&lock, &res, func, data](const double *xb, const double *xe) {
std::lock_guard<std::mutex> guard(lock);
const double partial = func(xb, xe, data); // must be guarded
res += partial;
},
xb_w, xe_w);
这首先让并行化毫无意义,不是吗?可能,从 software-engineering 的角度来看,更好的守卫位置是在包装函数 func
中——但我将其放入 worker
中,因为这样可以看到更好的结果。
Python 使用引用计数进行内存管理 - 类似于 std::shared_ptr
。然而它并没有像shared_ptr那样细粒度地锁定,它只在更改引用计数器时才锁定,而是使用更粗略的锁——全局解释器锁。结果是,当一个人从 open-MP-thread 或其他未在 Python-interpreter 中注册的线程更改 python-object 的引用计数时,reference-counter 不是 protected/guarded 并且出现竞争条件。您正在观察的是此类竞争条件的可能结果。
GIL 使您的努力或多或少变得不可能:您需要锁定每个可能的调用 python 但比序列化对此功能的调用更重要!