我是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
(地址边界错误)SIGABRT
或“ 我已经彻底检查了Cython的文档(我想),并且我在SO和Google上都搜索了此问题,但是我找不到适合该问题的解决方案。
基本上,我想拥有一个C库,它是Python的不了解,它使用来自多个线程的回调函数,该函数集成在Python中。这是可能吗?如Cython的documentation所述,我尝试了nogil
签名和with gil:
块,但是遇到编译错误。此外,Cython中与gc
相关的功能似乎仅对extension types有效,不适用于我的情况。
我被困住了,不胜感激。
答案 0 :(得分:1)
当您使用不带锁的Python对象/功能时会发生这种情况。关键部分不仅是求和,而且是对函数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);
首先使并行化变得毫无意义,不是吗?从软件工程的角度来看,最好的保护器应该是包装函数func
-但我将其放入worker
是因为这样可以更好地看到后果。>
Python使用引用计数进行内存管理-与std::shared_ptr
类似。但是,它不像shared_ptr那样以精细的粒度进行锁定,shared_ptr仅在更改参考计数器时锁定,而是使用更粗略的锁定-全局解释器锁定。结果是,当一个人从open-MP线程或其他未在Python解释器中注册的线程更改python对象的引用计数时,该引用计数器不受保护/保护,从而产生竞争条件。您正在观察的是这种比赛条件的可能结果。
GIL使您的工作或多或少成为不可能:您需要锁定对可能的python的每次调用,但要序列化对此功能的调用!