有没有更好的方法可以使此代码线程安全?线程局部静态似乎是一个生硬的工具



下面的代码模拟了一个更大的程序,该程序创建了一个模拟实例,然后使用firstprivate对其进行并行处理,以使我的实例私有。但是,实例本身在其方法中又创建了两个实例。

这个结构看起来很做作,但我有点束手束脚:类及其依赖性是由我想使用的工具决定的,我认为这种情况在科学计算界很常见。

它编译得很好,经过手动测试,似乎是线程安全的并且可以工作。

但我不确定我是否以最佳的方式使用C++技术,因为我怀疑我可以在内存层次结构中进一步声明实例,并使自己不必使用static变量,可能是通过引用我的另一个实例或类似的东西来传递在parallel区域中创建的实例。我怀疑这是最好的,因为#pragma omp parallel {}的大括号中的所有内容都是线程的本地内容。

因此,我的目标是为每个类创建两个(或更多)线程本地独立实例,特别是GenNo,因为它建模了一个随机数生成器,该生成器将每个线程播种一次,然后简单地用同一种子调用,尽管在这里我以可预测的方式更改我所称的"种子",只是为了了解程序的行为并揭示违反线程安全/竞赛条件的行为。

被注释掉的代码不起作用,但在程序以SIGSEGV 11.退出时产生了"分段错误"。我认为并行部署时,"唯一"指针并不是唯一的。总的来说,这个解决方案看起来更优雅,我想让它发挥作用,但很乐意听取您的意见。

为了获得std::unique_ptr功能,必须注释掉以thread_local static开头的行,并且必须删除其他注释。

#include <iostream>
#include <omp.h>
//#include <memory>
class GenNo
{
public:
int num;
explicit GenNo(int num_)
{
num = num_;
};
void create(int incr)
{
num += incr;
}
};
class HelpCrunch{
public:
HelpCrunch() {
}
void helper(int number)
{
std::cout << "Seed is " << number << " for thread number: " << omp_get_thread_num() << std::endl;
}
};
class calculate : public HelpCrunch
{
public:
int specific_seed;
bool first_run;
void CrunchManyNos()
{
HelpCrunch solver;
thread_local static GenNo RanNo(specific_seed);
//std::unique_ptr<GenNo> GenNo_ptr(nullptr);
/*
if(first_run == true)
{
GenNo_ptr.reset(new GenNo(specific_seed));
first_run = false;
}
solver.helper(GenNo_ptr->num);
*/
RanNo.create(1);
solver.helper(RanNo.num);

//do actual things that I hope are useful.
};
};


int main()
{
calculate MyLargeProb;
MyLargeProb.first_run = true;
#pragma omp parallel firstprivate(MyLargeProb)
{
int thread_specific_seed = omp_get_thread_num();
MyLargeProb.specific_seed = thread_specific_seed;
#pragma omp for
for(int i = 0; i < 10; i++)
{
MyLargeProb.CrunchManyNos();
std::cout << "Current iteration is " << i << std::endl;
}
}
return 0;
}

现在,带有thread_local static关键字的输出是:

Seed is 2 for thread number: 1
Current iteration is 5
Seed is 3 for thread number: 1
Current iteration is 6
Seed is 4 for thread number: 1
Current iteration is 7
Seed is 5 for thread number: 1
Current iteration is 8
Seed is 6 for thread number: 1
Current iteration is 9

Seed is 1 for thread number: 0
Current iteration is 0
Seed is 2 for thread number: 0
Current iteration is 1
Seed is 3 for thread number: 0
Current iteration is 2
Seed is 4 for thread number: 0
Current iteration is 3
Seed is 5 for thread number: 0
Current iteration is 4

在不使用thread_local但保留static的情况下,我得到:

Seed is 2 for thread number: 1
Current iteration is 5
Seed is 3 for thread number: 1
Current iteration is 6
Seed is 4 for thread number: 1
Current iteration is 7
Seed is 6 for thread number: 1
Current iteration is 8
Seed is 7 for thread number: 1
Current iteration is 9

Seed is 5 for thread number: 0
Current iteration is 0
Seed is 8 for thread number: 0
Current iteration is 1
Seed is 9 for thread number: 0
Current iteration is 2
Seed is 10 for thread number: 0
Current iteration is 3
Seed is 11 for thread number: 0
Current iteration is 4

如果我完全忽略了static关键字,实例就会不断地被重新分配,虽然我强烈怀疑它对线程是私有的,但它没有什么用处,因为在双核机器上,线程0和1的计数器会停留在1或2。(现实世界中的应用程序必须能够"向上计数",并且不受并行线程的干扰。)

我需要什么帮助

现在,我对这个例子进行了建模,通过计数器相互干扰,线程安全性的违反将变得明显,正如我们可以看到的那样,当thread_local被省略但static被保留时(两者都没有是愚蠢的)。HelpCrunch类实际上要复杂得多,并且很可能是线程安全的,并且可以在每次循环重复时重新初始化。(这实际上更好,因为它从它的子实例(一个私有实例)中提取了一堆变量。)但你认为我最好在创建solver时也添加thread_local,而不使用static关键字吗?或者我应该在其他地方声明实例,在这种情况下,我需要通过指针/引用等进行传递的帮助。

首先,您的示例以线程不安全的方式使用全局对象std::cout,从多个线程同时访问它。我不得不在某些地方添加#pragma omp critical以获得可读的输出。

其次,注释的代码崩溃是因为GenNo_ptr具有自动持续时间,因此每次CrunchManyNos()完成执行时都会销毁它。因此,当first_runfalse时,您正在解除对nullptr指针的围栏。

当涉及到您的特定问题时,制作RanNostaticstatic thread_local:之间存在巨大差异

  • 如果是static,则将存在RanNo的单个实例在第一次执行CCD_ 27时初始化。它是为了在某种程度上是一个全局变量,在示例的并发上下文。

  • 如果是static thread_local(顺便说一下,使用openmp,你应该更喜欢threadprivate),它会在新线程第一次调用CCD_ 30时创建将持续线程的持续时间。

最新更新