使用Cython和外部多线程C库时的未定义行为

时间:2019-05-23 21:41:02

标签: python-3.x multithreading cython

我是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))

通过未定义的行为,我的意思是我得到其中一个

  1. SIGSEGV(地址边界错误)
  2. 使用SIGABRT或“
  3. “严重的Python错误:GC对象已被跟踪”
  4. (很少)正确的结果。

我已经彻底检查了Cython的文档(我想),并且我在SO和Google上都搜索了此问题,但是我找不到适合该问题的解决方案。

基本上,我想拥有一个C库,它是Python的不了解,它使用来自多个线程的回调函数,该函数集成在Python中。这是可能吗?如Cython的documentation所述,我尝试了nogil签名和with gil:块,但是遇到编译错误。此外,Cython中与gc相关的功能似乎仅对extension types有效,不适用于我的情况。

我被困住了,不胜感激。

1 个答案:

答案 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的每次调用,但要序列化对此功能的调用!