这是我对代码多线程化的第一次尝试。
该代码由创建单独的Simulation对象的Simulation类组成。由于我需要运行其中的几个,因此我想在多个线程中并行运行它们。该代码在串行中运行良好,但是当将每个模拟对象方法分配给不同的线程时,我在不同的时间(通常很早)遇到分段错误,我认为这是由于某种形式的数据争夺所致。深入研究,我发现某些成员变量似乎被重新初始化或只是更改了值(并非每次运行都一致)。对我来说很明显,有些资源正在混杂在一起,但是当我在一个独立的线程中运行每个模拟时(或者我认为)怎么办?
这里是代码的简化版本。
模拟类:
class Simulation{
public:
void run(){
//Complicated stuff;
}
};
main.cpp:
int main(){
vector<Simulation> simulations;
vector<thread> threads;
for (int i=0; i<nSimulations; i++){
simulations.push_back(
Simulation(params));
threads.push_back(thread(&Simulation::run,
std::ref(simulations[i])));
}
for (int i=0; i<nSimulations; i++){
threads[i].join();
simulations[i].saveToFile("test.dat");
}
return 0;
}
这段代码有天生的错误吗?实际的代码非常复杂,所以至少我想知道这是否是将不同的对象方法多线程化到不同线程的正确方法。
答案 0 :(得分:2)
在处理std::vector
个元素的地址时,您应该非常谨慎,当您再push_back
个元素时,它们将会改变。
vector<Simulation> simulations;
for (int i=0; i<nSimulations; i++){
simulations.push_back(
Simulation(params));
threads.push_back(thread(&Simulation::run,
std::ref(simulations[i]))); // <-- This place !
}
此处将向量元素的地址保存在for
循环中,先前的地址在向量放大期间将无效。
答案 1 :(得分:1)
解决问题的最小更改是在启动任何线程之前构建所有仿真。
int main(){
vector<Simulation> simulations;
for (int i=0; i<nSimulations; i++){
simulations.push_back(Simulation(params)); // or emplace_back(params)
}
// or vector<Simulation> simulations(nSimulations, Simulation(params));
vector<thread> threads;
for (int i=0; i<nSimulations; i++){
threads.push_back(thread(&Simulation::run, std::ref(simulations[i])));
}
for (int i=0; i<nSimulations; i++){
threads[i].join();
simulations[i].saveToFile("test.dat");
}
return 0;
}
答案 2 :(得分:0)
现有的答案解决了所要求的简单情况,我们事先知道了模拟次数。解决方案是简单地在Simulation向量中保留足够的空间以使重新分配永远不会发生。
但是,如果不知道仿真次数,或者必须临时添加仿真怎么办?
一个答案可能是将模拟存储在std::list
中,而不是std::vector
中。但是,我们便失去了随机访问模拟的功能。
我们可以通过根据手柄/主体习惯用法实现Simulation来解决此问题。手柄是可移动的,并控制实际实现的生存期。
一个例子(在其中我也给运行模拟的概念一个类):
#include <memory>
#include <thread>
#include <vector>
struct SimulationParams {};
struct Simulation
{
// noncopyable
Simulation(Simulation const&) = delete;
Simulation& operator=(Simulation const&) = delete;
Simulation(SimulationParams params);
void run()
{
// complicated stuff
}
void saveToFile(std::string const& path);
};
class SimulationHandle
{
using impl_class = Simulation;
using impl_type = std::unique_ptr<impl_class>;
impl_type impl_;
public:
SimulationHandle(SimulationParams params)
: impl_(std::make_unique<impl_class>(std::move(params)))
{}
auto saveToFile(std::string const& path) -> decltype(auto)
{
return implementation().saveToFile(path);
}
auto runInThread() -> std::thread
{
return std::thread {
[&sim = this->implementation()]
{
sim.run();
}
};
}
auto implementation() -> impl_class&
{
return *impl_;
}
};
struct RunningSimulation
{
RunningSimulation(SimulationParams params)
: simHandle_{ std::move(params) }
, thread_ { simHandle_.runInThread() }
{
}
void join()
{
if (thread_.joinable())
thread_.join();
}
void saveToFile(std::string const& path)
{
join();
simHandle_.saveToFile(path);
}
private:
// DEPENDENCY: ORDER
// During constructor, thread_ depends on simHandle_ being constructed
SimulationHandle simHandle_;
std::thread thread_;
};
extern int nSimulations;
int main(){
using std::vector;
vector<RunningSimulation> simulations;
for (int i=0; i<nSimulations; i++)
simulations.emplace_back(SimulationParams());
for(auto&& rs : simulations)
rs.saveToFile("test.dat");
return 0;
}
其他增强功能:
当前句柄是根据unique_ptr
实现的-这意味着只有一个句柄可以拥有模拟。我们可能希望以多种方式索引模拟,这将需要多个句柄。
对此的一种可能的解决方案是简单地将unique_ptr
替换为shared_ptr
,以实现共享所有权。另一个可能是具有生存期控制句柄(由shared_ptr
实现)和生存期监视器(由weak_ptr
实现)的概念。