MATLAB parfor和C++类mex包装器(是否需要复制构造函数?)



我正试图使用这里概述的方法将C++类封装在matlab mex包装器中。基本上,我有一个初始化mex文件,它返回一个C++对象句柄:

handle = myclass_init()

然后,我可以将其传递给另一个使用句柄调用类方法的mex文件(例如myclass_amethod),然后最终传递给myclass_delete以释放C++对象:

retval = myclass_amethod(handle, parameter)
myclass_delete(handle)

为了便于使用,我已经将其封装在MATLAB类中:

classdef myclass < handle
properties(SetAccess=protected)
cpp_handle_
end
methods
% class constructor
function obj = myclass()
obj.cpp_handle_ = myclass_init();
end
% class destructor
function delete(obj)
myclass_delete(obj.cpp_handle_);
end
% class method
function amethod(parameter)
myclass_amethod(obj.cpp_handle_, parameter);
end
end
end

问题:这在并行代码中不起作用

这在非并行代码中运行良好。然而,一旦我从parfor:中调用它

cls = myclass();
parfor i = 1:10
cls.amethod(i)
end

我得到了一个segfault,因为类的副本是在parfor循环中(在每个工作进程中)生成的,但由于每个工作进程是一个单独的进程,所以C++对象实例被而不是复制,导致指针无效。

最初,我试图检测每个类方法何时在parfor循环中运行,在这些情况下,也会重新分配C++对象。然而,由于无法检查对象是否已分配给当前工作进程,这会导致多次重新分配,然后仅删除一次(当工作进程退出时),从而导致内存泄漏(有关详细信息,请参阅问题底部的附录)。

尝试的解决方案:复制构造函数并使用matlab.mixin.Copyable

在C++中,处理这一问题的方法是复制构造函数(这样,当复制包装器MATLAB类时,C++对象只会重新分配一次)。快速搜索会出现matlab.mixin.Copyable类类型,它似乎提供了所需的功能(即matlab句柄类的深层副本)。因此,我尝试了以下方法:

classdef myclass < matlab.mixin.Copyable
properties(SetAccess=protected)
cpp_handle_
end
methods
function obj = myclass(val)
if nargin < 1
% regular constructor
obj.cpp_handle_ = rand(1);
disp(['Initialized myclass with handle: ' num2str(obj.cpp_handle_)]);
else
% copy constructor
obj.cpp_handle_ = rand(1);
disp(['Copy initialized myclass with handle: ' num2str(obj.cpp_handle_)]);
end
end
% destructor
function delete(obj)
disp(['Deleted myclass with handle: ' num2str(obj.cpp_handle_)]);
end
% class method
function amethod(obj)
disp(['Class method called with handle: ' num2str(obj.cpp_handle_)]);
end
end
end

如上所述测试此类,即:

cls = myclass();
parfor i = 1:10
cls.amethod(i)
end

输出结果:

Initialized myclass with handle: 0.65548
Class method called with handle: 0.65548
Class method called with handle: 0.65548
Class method called with handle: 0.65548
Class method called with handle: 0.65548
Class method called with handle: 0.65548
Class method called with handle: 0.65548
Class method called with handle: 0.65548
Class method called with handle: 0.65548
Class method called with handle: 0.65548
Class method called with handle: 0.65548
Deleted myclass with handle: 0.65548
Deleted myclass with handle: 0.65548
Deleted myclass with handle: 0.65548

换句话说,在生成parfor的工作程序时,复制构造函数似乎不是调用的。有人知道我做错了什么吗?或者在复制MATLAB包装类时,是否有某种方法可以实现重新初始化C++对象句柄的预期行为?


替代方法:在工人身上运行时进行检测

仅供参考,以下是我在员工中使用的另一种重新分配方法:

classdef myclass < handle
properties(SetAccess=protected)
cpp_handle_
end
methods
function obj = myclass(val)
obj.cpp_handle_ = rand(1);
disp(['Initialized myclass with handle: ' num2str(obj.cpp_handle_)]);
end
% destructor
function delete(obj)
disp(['Deleted myclass with handle: ' num2str(obj.cpp_handle_)]);
end
% class method
function amethod(obj)
obj.check_handle()
disp(['Class method called with handle: ' num2str(obj.cpp_handle_)]);
end
% reinitialize cpp handle if in a worker:
function check_handle(obj)
try
t = getCurrentTask();
% if 'getCurrentTask()' returns a task object, it means we
% are running in a worker, so reinitialize the class
if ~isempty(t)
obj.cpp_handle_ = rand(1);
disp(['cpp_handle_ reinitialized to ' num2str(obj.cpp_handle_)]);
end
catch e
% in case of getCurrentTask() being undefined, this
% probably simply means the PCT is not installed, so
% continue without throwing an error
if ~strcmp(e.identifier, 'MATLAB:UndefinedFunction')
rethrow(e);
end
end
end
end
end

输出:

Initialized myclass with handle: 0.034446
cpp_handle_ reinitialized to 0.55625
Class method called with handle: 0.55625
cpp_handle_ reinitialized to 0.0048098
Class method called with handle: 0.0048098
cpp_handle_ reinitialized to 0.58711
Class method called with handle: 0.58711
cpp_handle_ reinitialized to 0.81725
Class method called with handle: 0.81725
cpp_handle_ reinitialized to 0.43991
cpp_handle_ reinitialized to 0.79006
cpp_handle_ reinitialized to 0.0015995
Class method called with handle: 0.0015995
cpp_handle_ reinitialized to 0.0042699
cpp_handle_ reinitialized to 0.51094
Class method called with handle: 0.51094
Class method called with handle: 0.0042699
Class method called with handle: 0.43991
cpp_handle_ reinitialized to 0.45428
Deleted myclass with handle: 0.0042699
Class method called with handle: 0.79006
Deleted myclass with handle: 0.43991
Deleted myclass with handle: 0.79006
Class method called with handle: 0.45428

正如上面所看到的,重新分配现在确实发生在运行工作者中。但是,无论C++类被重新分配了多少次,析构函数对于每个工作线程只调用一次,从而导致内存泄漏。


解决方案:使用loadobj

以下工作:

classdef myclass < handle
properties(SetAccess=protected, Transient=true)
cpp_handle_
end
methods(Static=true)
function obj = loadobj(a)
a.cpp_handle_ = rand(1);
disp(['Load initialized encoder with handle: ' num2str(a.cpp_handle_)]);
obj = a;
end
end
methods
function obj = myclass(val)
obj.cpp_handle_ = rand(1);
disp(['Initialized myclass with handle: ' num2str(obj.cpp_handle_)]);
end
% destructor
function delete(obj)
disp(['Deleted myclass with handle: ' num2str(obj.cpp_handle_)]);
end
% class method
function amethod(obj)
disp(['Class method called with handle: ' num2str(obj.cpp_handle_)]);
end
end
end

当您将对象实例传递到PARFOR循环的主体中时,其行为与您将其保存到文件中,然后再次加载它的行为相同。最简单的解决方案可能是将您的cpp_handle_标记为Transient。然后,您需要实现SAVEOBJLOADOBJ来安全地传输数据。有关自定义类的保存/加载行为的更多信息,请参阅本页。

最新更新