3

我只是花了一段时间为别人的问题写了一个很长的答案,只是为了在我发布答案之前将其删除。不想浪费精力,所以我在这里发布问题和答案。

这不仅仅是关于发送/接收死锁的标准答案,因为我还发现了一个有趣的半解决方案,它只适用于某些编译器

在并行课程中,我们需要做一个基于主从设计模式的练习,其中主进程 0 向他的所有从属进程发送一条消息,这些从属进程将向它们的左右邻居重新发送消息(处理器 id +/- 1,除了没有左邻居的处理器 0 和没有右邻居的最后一个处理器 id)。在将消息重新传递给邻居之后,从处理器向主处理器发送作业结束的确认。

练习很简单,但我的代码存在问题,因为我在程序开始时收到确认结束消息......我不明白这里有什么问题。我尝试使用 fflush,但实际上程序的最后一行应该在接收之后才写入控制台。

有人有什么想法吗?我是 MPI/C 概念的新手,所以我的工作可能有问题?

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mpi.h>

int main(int argc, char *argv[]){
    int np, myId;
    char send[100], recv[100];

    MPI_Init(&argc, &argv);

    MPI_Comm_size(MPI_COMM_WORLD, &np);
    MPI_Comm_rank(MPI_COMM_WORLD, &myId);

    MPI_Status stat;
    if(myId == 0){
        int t = sprintf(send, "hey!"); //MPI_get_processor_name
        for(int i = 1; i < np; i++){
            printf("send %d => %d\n", myId, i);
            fflush(stdout);
            MPI_Send(send, 50, MPI_CHAR, i, 0, MPI_COMM_WORLD);
        }

        for(int i = 1; i < np; i++){
            MPI_Recv(recv, 50, MPI_CHAR, i, 0, MPI_COMM_WORLD, &stat);
            printf("%s\n", recv);
            fflush(stdout);
        }


    }else{
        if(myId < (np - 1)){
            printf("send %d => %d\n", myId, myId + 1);
            fflush(stdout);
            MPI_Send(send, 50, MPI_CHAR, myId + 1, 0, MPI_COMM_WORLD);
        }

        if(myId > 1){
            printf("Envoie %d => %d\n", myId, myId - 1);
            fflush(stdout);
                    MPI_Send(send, 50, MPI_CHAR, myId - 1, 0, MPI_COMM_WORLD);
        }

        MPI_Recv(send, 50, MPI_CHAR, MPI_ANY_SOURCE, 0, MPI_COMM_WORLD, &stat); 

        printf("Réception %d <= %d\n", myId, 0);
        fflush(stdout);

        if(myId != (np - 1)){
            MPI_Recv(send, 50, MPI_CHAR, myId + 1, 0, MPI_COMM_WORLD, &stat);
            printf("Receive %d <= %d\n", myId, myId + 1);
            fflush(stdout);
        }

        if(myId != 1){
            MPI_Recv(send, 50, MPI_CHAR, myId - 1, 0, MPI_COMM_WORLD, &stat);
            printf("Receive %d <= %d\n", myId, myId - 1);
            fflush(stdout);
        }

        int t = sprintf(recv, "End for %d.", myId);
        MPI_Send(recv, 50 , MPI_CHAR, 0, 0, MPI_COMM_WORLD); 
    }

    MPI_Finalize();
    return 0;
}
4

1 回答 1

6

解决方案 1

让我们比较一下所有非 0 的“从属”内核实际上在做什么和你说他们应该做什么。

你希望他们做什么:

主进程 0 向他的所有从属进程发送一条消息,该从属进程将向其左右邻居重新发送消息(处理器 id +/- 1,除了没有左邻居的处理器 0 和最后一个没有左邻居的处理器 id '没有正确的邻居)。在将消息重新传递给邻居之后,从处理器向主处理器发送作业结束的确认。

代码大纲:

Send_To_Right_Neighbour();

Send_To_Left_Neighbour();

Receive_From_Master();

Receive_From_Right_Neighbour();

Receive_From_Left_Neighbour();

Send_To_Master();

看到不同?奴隶在重新发送给他们的邻居之前没有收到来自主人的信息。将代码更改为:

Receive_From_Master();

Send_To_Right_Neighbour();

Send_To_Left_Neighbour();

Receive_From_Right_Neighbour();

Receive_From_Left_Neighbour();

Send_To_Master();

将解决该问题,然后代码为我运行完成。

出了什么问题

MPI_Send 可以是一个阻塞函数——即调用MPI_Send不会返回,直到另一个进程调用了一个匹配MPI_Recv(尽管它不一定一个阻塞函数)。您应该假设在编写代码时它总是会阻塞。

现在让我们想象一下当您使用 >5 个进程运行时,非 0 进程会做什么。

  • 进程 1 发送到它的右邻居(进程 2),并在那里等待直到进程 2 调用MPI_Recv
  • 进程 2 发送到它的右邻居(进程 3),并在那里等待直到进程 3 调用MPI_Recv
  • 进程 3 发送到它的右邻居(进程 4),并在那里等待直到进程 4 调用MPI_Recv
  • ...
  • 进程 n-2 发送到它的右邻居(进程 n-1),并在那里等待直到进程 n-1 调用MPI_Recv
  • 进程 n-1 没有右邻居,因此继续发送到左邻居,并在那里等待直到进程 n-2 调用MPI_Recv

这永远不会发生,因为进程 n-2在尝试从 n-1 接收数据之前正忙于等待进程 n-1 接收其数据。这是一个僵局,任何一个进程都不会让步。

为什么解决方案有效

我已经说过上述解决方案对我有用——但它并不完美。我所做的唯一更改是将接收从进程 0 移到第一步——为什么这会影响死锁?

答案是它根本不应该影响死锁。我的猜测是编译器已经足够聪明地意识到每个核心都在向同一个邻居发送和接收,并将对左右邻居的单独MPI_Send和调用组合成调用。这在同一步骤中向邻居发送和接收,从而摆脱了死锁问题。以前,从 0 开始的接收调用是在发送和接收到同一个邻居之间进行的,因此编译器无法将其优化为单个操作。MPI_RecvMPI_Sendrecv

但是我们不想依赖一个好的编译器——你的代码应该在任何符合标准的编译器上工作——所以我们应该自己手动修复死锁问题,而不是依赖编译器的聪明。

解决方案 2

首先,对您目前在课程中可能涵盖或未涵盖的内容的一些评论

  • 进程 0 正在向所有其他核心发送相同的信息。如果你知道MPI_Bcast你应该使用它而不是所有这些发送和接收。
  • 进程 0 最后从所有其他核心接收。如果您愿意接收多个 char 数组,您可以非常简单地使用MPI_Gather.
  • 我真的不明白主进程向每个其他进程发送一些数据的逻辑,然后每个进程将相同的数据共享给它的每个邻居(主进程已经给了它)。如果共享的数据有所不同,或者主进程仅将数据发送给某些从属进程,并且它们必须在彼此之间共享,那对我来说会更有意义。

也就是说,让我们谈谈避免僵局。因此,根本问题是我们必须确保无论MPI_Send一个进程调用什么,另一个进程都可以同时调用匹配MPI_Recv而不必等待发送进程执行其他任何操作。问题出在每个核心试图同时发送。

因此,我们可以解决的一种方法是首先确定信息将完全朝一个方向移动。我选择了从左到右。在这种情况下,每个从内核必须执行以下操作:

Receive_From_Master();

// Make sure all info is sent from left to right
Send_To_Right_Neighbour();
// Make sure any info is received from left to right
Receive_From_Left_Neighbour();

// Now send all info from right to left
Send_To_Left_Neighbour();
// Make sure any info is received 
Receive_From_Right_Neighbour();

Send_To_Master();

现在发生的事情是这样的:

  • 进程 2 开始发送到进程 3
  • 进程 3 开始发送到进程 4
  • ...
  • 进程 n-2 开始发送到进程 n-1
  • 进程 n-1 没有右邻居,因此继续从进程 n-2 接收
  • 进程 n-2 完成向进程 n-1 的发送,因此继续从进程 n-3 接收
  • ...
  • 进程 3 完成向进程 4 的发送并继续从进程 2 接收。

从左到右发送时也会发生同样的情况,除了现在,进程 1 没有左邻居要发送到 ,因此可以直接从进程 2 接收。在任何一种情况下都不会出现死锁。

于 2012-10-16T20:39:33.013 回答