OpenMP 中树结构的线程安全

Posted

技术标签:

【中文标题】OpenMP 中树结构的线程安全【英文标题】:Thread safety of tree structures in OpenMP 【发布时间】:2019-01-14 23:12:12 【问题描述】:

我有一个基于 Barnes-Hut 算法的 N-Body 模拟器,我使用 OpenMP 进行了多线程处理。大多数程序都是通过在几个关键位置简单地添加#pragma omp parallel for 来实现的。这提供了一个健康的加速,当引力体的数量低于几千时,它可以很好地与核心数量相匹配。

因为我的程序使用Barnes-Hut algorithm,它的核心是一个树结构,在二维中这是一个四叉树,在我的例子中是一个八叉树。我在多线程填充树的过程中遇到了麻烦。将此步骤设为单线程会阻止程序充分利用我的处理器。我添加的主体越多,我的 CPU 使用率实际上就会下降,因为只使用一个核心将所有主体添加到八叉树所花费的时间更多。

现在向八叉树添加单个主体的方法如下所示:

void octant::addBody(vec3 newPosition, float newMass) 

    // Making room for new bodies by dividing if the node is a leaf
    if (isLeaf) 

        // Subdividing the octant
         divide();

        // Moving the body already contained
        subdivisionEnclosing(this->position)->addBody(this->position, this->mass);
    

    // Adding the body to the appropriate subdivision if the node is divided
    if (divided) 

        // Adding the new body to the appropriate octant
        subdivisionEnclosing(newPosition)->addBody(newPosition, newMass);

        return;
    

    // If the node doesnt yet contain any bodies at all, add the new one
    this->position = newPosition;
    this->mass = newMass;

    // This node only contains one body, so the center of mass is accurate
    isLeaf = true;
    calculatedCOM = true;

这在串联调用时工作得很好,但当我尝试同时将多个主体添加到同一个根节点时自然会崩溃。此代码不包含任何使八分圆对象线程安全的措施。

理想情况下,我希望能够使用类似这样的方法并行调用 addBody 方法:

#pragma omp parallel for
for (int b = 0; b < bodies.size(); ++b) 
    octree->addBody(bodies[b]->getPosition(), bodies[b]->getMass());

我已经尝试将#pragma omp critical(name) 添加到更改数据的部分方法和细分节点的#pragma omp single。我没有尝试阻止立即发生段错误。

我还构建了一个批量添加主体的方法。它接收一个身体对象的向量,根据它们适合的细分将它们分类为向量,然后将这些向量传递到它们各自的细分中。每个细分都有自己的线程,并且该过程是递归的。这运行并使用了我所有的核心,但速度明显较慢。我认为将身体放入向量中会增加大量开销。

我对 OpenMP 还很陌生,甚至对线程安全的概念也很陌生。解决这个问题的最佳方法是什么?我似乎无法在网上找到很多线程安全树结构的示例,也没有使用 OpenMP。使用多线程填充树的理想方法是什么?最起码,您认为哪些工具可以让这种事情发挥作用?

编辑: 有谁知道完全线程安全的树结构的任何示例?即使它不在 OpenMP 中,我主要对如何以线程安全的方式添加/生成/填充树感兴趣。

【问题讨论】:

也许你应该看看openmp3.0中引入的omp任务。它们是专门为列表、树遍历等引入的。 我去看看!如果我最终分批处理尸体,任务可能会很有用。我的主要问题是我不只是遍历树。随着主体的添加,树改变了结构,所以我最终可能会有多个线程试图同时访问和更改一条数据。我是否在正确的道路上使用 omp 关键标志来防止这种情况? 【参考方案1】:

为了使写入操作的树线程安全(例如在您的示例中添加节点),我只能想到锁定算法 - 例如Two-phase locking。例如,这些结构用于数据库。想法是沿着树向下,找出需要添加节点的位置,它将影响哪些(所有)其他父节点,等待锁定这些节点,锁定它们,执行添加操作并解锁。这将始终使树保持一致状态,同时允许在树的不同部分进行并发添加操作。因此,在您考虑实现这一点之前,先看看如何将数据添加到树中。如果大多数添加会发生冲突,那么锁定的开销不会超过加速所带来的收益。

还有几个 cmets。 @Joseph Franciscus 的意思是并行进行大部分计算,然后按顺序将所有节点添加到树中,如果您不期望节点数量达到数十亿,应该可以正常工作。

但是,您可以扩展他的想法。您可以实现类似于并行生产-消费模式的东西。任意数量的工作线程将用于创建主体并将结果放入线程安全队列中,并且只有一个线程(!)将添加它们。通过这种方式,您可以使两个工作相互交织,并并行完成更多工作。

PS。 omp parallel for 之后的障碍是隐含的,你不需要把它放在那里 AFAIK。

编辑: 我在想也许一些伪 C 代码会有所帮助:

#pragma omp parallel sections num_threads(2)

  #pragma omp section
  
    while (true) 
      if (queue_notEmpty())
        if (node is last) break;
        node = queue_front(); queue_pop();
        tree->addNode(node);
      
    
  
  #pragma omp section
  
     #pragma omp parallel for
     for (int i = 0; i < N; ++i) 
        node = init_node(...);
        queue_push(node);
     
  

这将首先产生两个线程,每个线程占用一个部分。然后在第二部分中将产生更多线程,您还可以使用num_thread 属性来控制它。我能想到的唯一警告是如何使线程将节点放入树端。您可以在队列中放入一个特殊节点,表示不再添加节点。

我写的伪代码也有所谓的主动等待。它一直询问队列是否为空。您可以通过使用信号量向消费者线程发出信号来摆脱它。取决于线程需要等待多少数据。你也可以尝试一下。

标准库队列/双端队列不是线程安全的,因此请确保实现您自己的或使用为在并行场景中使用而制作的库。希望成功!

【讨论】:

这对我来说很有意义!我想一旦我开始工作,我会将此标记为正确的。【参考方案2】:

这只是关于如何实施的建议。 我相信有很多方法可以解决这个问题。

void octant::addBody(Body);
Body octant::create_body(vec3 newPosition, float newMass);

int main()  

    int thread_count = omp_get_num_threads();
    std::vector<std::vector<Body>> body_list(thread_count);  //each thread gets its own list of bodies

    #pragma omp parallel for
    for (int b = 0; b < bodies.size(); ++b) 
        int index = omp_get_thread_num();
        Body tmp = octant::create_body(bodies[b]->getPosition(), bodies[b]->getMass());

        body_list[index].push_back(tmp); 
    
    #pragma omp barrier    //make sure to add barrier (as openmp is asynchronous to host thread)

    for (int i = 0; i < thread_count; ++i) 
        for (int j = 0; j < body_list[i].size(); ++j) 
             bodies.add_body(body_list[i][j]);
    

基本上,您首先创建实体,然后将它们添加到平行部分之后。这确保您不会出现段错误并提供近似的速度线性加速(假设大部分成本是创建主体,而不是添加它们)。

【讨论】:

我实际上在程序开始时将所有物体添加到一个向量中。在每帧更新模拟之前使用 octant::addbody 方法。我只将每个物体的位置和质量传递到八叉树中,因为这就是计算每个八分圆的质心所必需的。实际的“body”对象包含颜色和半径等信息,用于在 OpenGL 中绘制身体。如果我分批处理身体,屏障标志确实有意义,它可以让我分开接收身体并递归地将它们传递到细分中。感谢您的意见!

以上是关于OpenMP 中树结构的线程安全的主要内容,如果未能解决你的问题,请参考以下文章

JNI 函数是线程安全的吗?

我可以安全地将 OpenMP 与 C++11 一起使用吗?

如何使用多线程处理缓存的数据结构(例如 openmp)

c ++ OpenMP关键:“单向”锁定?

线程安全与JVM内存结构

HashMap结构,线程不安全