OpenMP中树结构的线程安全

时间:2019-01-14 23:12:12

标签: c++ multithreading tree thread-safety openmp

我有一个基于Barnes-Hut算法的N体模拟器,该算法已使用OpenMP多线程处理。只需在几个关键位置添加#pragma omp parallel for即可使大多数程序并行执行。这提供了一个健康的加速比,当重力体的数量小于一千时,该加速比与核的数量成比例。

因为我的程序使用Barnes-Hut algorithm,所以它的核心是树结构,在2d中,这是四叉树,在我的情况下是八叉树。我在多线程填充树的过程中遇到麻烦。使此步骤为单线程可防止程序充分利用我的处理器。实际上,我的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中,我也主要对如何以线程安全的方式将树添加/生成/填充感兴趣。

2 个答案:

答案 0 :(得分:0)

这只是有关如何执行此操作的建议。 我敢肯定,有多种方法可以解决这个问题。

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]);
    }
}

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

答案 1 :(得分:0)

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

更多评论。如果您不希望节点数量达到数十亿个,那么@Joseph Franciscus并行进行大量计算然后将所有节点顺序添加到树中的意思应该很好。

但是,您可以扩展他的想法。您可以实现类似于并行的Produce-Consume模式的东西。任意数量的工作线程将在创建主体时工作,并将结果放入线程安全的队列中,并且只有一个线程(!)会添加它们。这样,您可以使两个作业相互缠绕,并并行完成更多工作。

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属性来控制线程。我唯一想到的警告是如何使线程将节点放入树末端。您可以在队列中放入一个特殊的节点,表示不再添加任何节点。

我编写的伪代码也称为主动等待。它始终询问队列是否为空。您可以通过使用信号量通知使用者线程来摆脱它。取决于线程需要等待多少数据。您也可以尝试一下。

标准库队列/双端队列不是线程安全的,因此请确保实现您自己的库或使用为并行场景使用的库。希望能解决!