多线程c++应用程序中的Fortran 77通用块

Fortran 77 common blocks in multithreading C++ application

本文关键字:77通 Fortran c++ 应用程序 多线程      更新时间:2023-10-16

我开发了一个调用Fortran 77例程的c++程序。主c++程序可以多线程运行。然而,碰巧Fortran 77例程隐藏了几个公共块,这些块在每次调用时根据其参数进行修改。

我担心所有的公共块可能在多个线程之间共享,并发访问这些块可能会把一切搞砸。

  • 第一个问题:我说的对吗?公共块会在多个线程之间共享吗?

  • 第二个问题:是否有一个简单的方法来避免它?重写Fortran例程似乎负担不起,我正在寻找一种方法,以便每个线程都有自己的所有公共块的副本(这些块并不大,应该可以快速复制)。我不知道编译选项是否会有帮助,或者OpenMP是否可以帮助我。

是的,公共块是共享的。

在OpenMP中,可以将公共块指定为THREADPRIVATE。每个线程动态创建一个公共块的新实例。要从原始数据中复制数据,请使用COPYIN说明符。参见OpenMP threadprivate和private的区别 基本语法是
!$OMP THREADPRIVATE (/cb/, ...)  

,其中cb是公共块的名称。参见https://computing.llnl.gov/tutorials/openMP/#THREADPRIVATE

你是正确的,普通块不是线程安全的。它们是全局数据,允许您在任何范围单元中声明所有共享相同存储关联的变量。如果你在c++中写全局变量,结果基本上是一样的,因为所有的线程同步问题都会导致。

不幸的是,我认为没有一个简单的方法来避免它。如果您需要维护多线程方法,我过去看到的一个想法是将所有变量从公共块移动到用户定义的类型中,并将该类型的实例传递给需要访问它们的任何过程(每个线程一个实例)。但是,这可能涉及到对代码进行昂贵的修改。

您还需要查看Fortran代码的其他线程安全问题(这不是一个详尽的列表):

  • IO单元应该是每个线程唯一的,否则文件输入/输出将不可靠
  • 任何带有SAVE属性的变量(在模块变量和声明时初始化的变量中隐式)都是有问题的(这些变量在过程调用之间是持久的)。这个属性的隐式性也依赖于编译器/标准,这使得它成为一个更大的潜在问题。
  • 声明带有RECURSIVE属性的过程——这意味着函数是可重入的。这也可以通过编译器的openmp选项来满足,而不是修改代码。

您可以探索的另一种方法是使用多处理或消息传递来并行化代码,而不是使用多线程。这避免了Fortran代码的线程安全问题,但提出了另一个可能代价高昂的代码体系结构更改。

也看到:

  • https://software.intel.com/en-us/forums/topic/276535
  • https://software.intel.com/en-us/forums/topic/270572

是的,你不能使用多线程的公共区域。不,这是无法避免的。所有公共区域实际上都被链接器合并到一个块中,并且没有办法在线程之间复制它。在遗留Fortran代码存在的任何地方,这都是一个众所周知的问题。最常见的解决方案是使用多处理而不是多线程。

感谢您的回答,特别是关于OpenMP的提示,它确实是可行的。为了完全确定,我编写了一个小程序。它由一个fortran 77部分组成,在一个主要的c++程序中调用(这是我关心的):

fortran 77例程函数。f :

  subroutine set(ii, jj)
  implicit none
  include "func.inc"
  integer ii, jj
  integer OMP_GET_NUM_THREADS, OMP_GET_THREAD_NUM
  i = ii + 1
  j = jj
  !$OMP CRITICAL
  print *, OMP_GET_THREAD_NUM(), OMP_GET_NUM_THREADS(), i, j
  !$OMP END CRITICAL
  return
  end

  subroutine func(n, v)
  implicit none
  include "func.inc"
  integer n, k
  integer v(n)
  do k = i, j
     a = k + 1
     b = a * a
     c = k - 1
     v(k) = b - c * c
  enddo
  return
  end

与include file 函数。公司

  integer i, j
  integer a, b, c
  common /mycom1/ i, j
  !$OMP THREADPRIVATE(/mycom1/)
  common /mycom2/ a, b, c
  !$OMP THREADPRIVATE(/mycom2/)
最后是c++程序main.cpp:
#include<iostream>
#include<sstream>
#include<vector>
using namespace std;
#include<omp.h>
extern "C"
{
  void set_(int*, int*);
  void func_(int*, int*);
};

int main(int argc, char *argv[])
{
  int nthread;
  {
    istringstream iss(argv[1]);
    iss >> nthread;
  }
  int n;
  {
    istringstream iss(argv[2]);
    iss >> n;
  }
  vector<int> a(n, -1);
#pragma omp parallel num_threads(nthread) shared(a)
  {
    const int this_thread = omp_get_thread_num();
    const int num_threads = omp_get_num_threads();
    const int m = n / num_threads;
    int start = m * this_thread;
    int end = start + m;
    const int p = n % num_threads;
    for (int i = 0; i < this_thread; ++i)
      if (p > i) start++;
    for (int i = 0; i <= this_thread; ++i)
      if (p > i) end++;
#pragma omp critical
    {
      cout << "#t " << this_thread << " : [" << start
           << ", " << end << "[" << endl;
    }
    set_(&start, &end);
    func_(&n, a.data());
  }
  cout << "[ " << a[0];
  for (int i = 1; i < n; ++i)
    cout << ", " << a[i];
  cout << "]" << endl;
  ostringstream oss;
  for (int i = 1; i < n; ++i)
    if ((a[i] - a[i - 1]) != int(4))
      oss << i << " ";
  if (! oss.str().empty())
    cout << "<<!!  Error occured at index " << oss.str()
         << " !!>>" << endl;
  return 0;
}
  • 编译步骤(gcc版本4.8.1):

    gfortran -c func.f -fopenmp
    g++ -c main.cpp  -std=gnu++11 -fopenmp
    g++ -o test main.o func.o -lgfortran -fopenmp
    
  • 你可以这样启动它:

    ./test 10 1000
    

    ,

      第一个整数(10)是你想要的线程数,
  • 第二个(1000)是一个矢量的长度。

这个程序的目的是在线程之间分割这个向量并让每条线程填充其中的一部分。

向量的填充是在fortran 77:

  • set例程首先设置线程管理的下限和上限,
  • func例程然后填充前面边界之间的向量。

通常情况下,如果没有错误并且没有共享公共fortran 77块,则最终向量应该填充4 * k值,k从1到1000。

我抓不到程序。相反,如果我在函数中删除fortran 77 OMP指令。. inc,那么公共块不再是私有的,并且会出现许多错误。

所以总结,我需要做的唯一一件事来解决我的初始问题是添加OMP指令后面的任何公共块,希望不是太复杂,因为它们都聚集在一个包含文件(像我的测试)。

希望这对你有帮助。

问好。