多线程 C++ 应用程序中的 Fortran 77 公共块
Fortran 77 common blocks in multithreading C++ application
我开发了一个调用 Fortran 77 例程的 C++ 程序。主 C++ 程序可能 运行 多线程。但是,Fortran 77 例程碰巧隐藏了几个公共块,这些块在每次调用时都会根据其参数进行修改。
恐怕所有公共块都可能在多个线程之间共享,并且对这些块的并发访问可能会弄乱一切。
第一个问题:我说得对吗?公共块是否会在多个线程之间共享?
第二个问题:有没有简单的方法可以避免呢?重写 Fortran 例程似乎负担不起,我宁愿寻找一种方法,以便每个线程都有自己的所有公共块的副本(它们不大,应该可以快速复制)。我不知道编译选项是否有帮助,或者 OpenMP 是否可以帮助我。
你说公共块不是线程安全的是对的。它们是全局数据,允许您在所有共享相同存储关联的任何作用域单元中声明变量。如果您在 C++ 中写入全局变量,并且会导致所有线程同步问题,则效果基本相同。
不幸的是,我认为没有简单的方法可以避免它。如果您需要维护多线程方法,我过去看到的一个想法是将所有变量从公共块移动到用户定义的类型,并将该类型的实例传递给需要访问的任何过程给他们(每个线程一个实例)。不过,这将涉及对要实施的代码进行潜在的昂贵更改。
您还需要查看 Fortran 代码的其他线程安全问题(这不是一个详尽的列表):
- 每个线程的 IO 单元应该是唯一的,否则文件 input/output 将不可靠
- 任何具有
SAVE
属性的变量(隐含在模块变量和声明时初始化的变量中)都是有问题的(这些变量在过程调用之间是持久的)。此属性的隐含性也 compiler/standard 相关,这使它成为一个更大的潜在问题。
- 使用
RECURSIVE
属性声明过程——这意味着该函数是可重入的。这也可以通过使用编译器 openmp 选项进行编译而不是更改代码来满足。
您可以探索的另一条路线是使用多处理或消息传递来并行化您的代码,而不是多线程。这避免了 Fortran 代码的线程安全问题,但带来了另一个可能代价高昂的代码架构更改。
另见:
是的,您不能将公共区域与多线程一起使用。不,没有办法避免这种情况。所有公共区域实际上都被链接器合并为单个块,并且无法在线程之间复制它。在存在遗留 Fortran 代码的任何地方,这都是一个已知问题。最常见的解决方案是使用多处理而不是多线程。
是的,公共块是共享的。
在 OpenMP 中,可以将公共块指定为 THREADPRIVATE。然后每个线程动态地创建公共块的新实例。要从原始数据复制数据,请使用 COPYIN 说明符。另见 Difference between OpenMP threadprivate and private
基本语法是
!$OMP THREADPRIVATE (/cb/, ...)
其中 cb 是公共块的名称。参见 https://computing.llnl.gov/tutorials/openMP/#THREADPRIVATE
感谢您的回答,特别是关于OpenMP的提示,确实可以。为了完全确定,我做了一个小程序。它由一个在一个主要 C++ 程序中调用的 Fortran 77 部分组成(这是我关心的):
fortran 77 例程func.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
包含文件 func.inc
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。
我无法捕捉到程序。相反,如果我在 func.inc 中删除 fortran 77 OMP 指令,那么公共块不再是私有的,并且会出现很多错误。
总而言之,我唯一需要做的就是在任何公共块后面添加 OMP 指令,希望这不会太复杂,因为它们都聚集在一个包含文件中(比如我的测试)。
希望这对您有所帮助。
此致。
我开发了一个调用 Fortran 77 例程的 C++ 程序。主 C++ 程序可能 运行 多线程。但是,Fortran 77 例程碰巧隐藏了几个公共块,这些块在每次调用时都会根据其参数进行修改。
恐怕所有公共块都可能在多个线程之间共享,并且对这些块的并发访问可能会弄乱一切。
第一个问题:我说得对吗?公共块是否会在多个线程之间共享?
第二个问题:有没有简单的方法可以避免呢?重写 Fortran 例程似乎负担不起,我宁愿寻找一种方法,以便每个线程都有自己的所有公共块的副本(它们不大,应该可以快速复制)。我不知道编译选项是否有帮助,或者 OpenMP 是否可以帮助我。
你说公共块不是线程安全的是对的。它们是全局数据,允许您在所有共享相同存储关联的任何作用域单元中声明变量。如果您在 C++ 中写入全局变量,并且会导致所有线程同步问题,则效果基本相同。
不幸的是,我认为没有简单的方法可以避免它。如果您需要维护多线程方法,我过去看到的一个想法是将所有变量从公共块移动到用户定义的类型,并将该类型的实例传递给需要访问的任何过程给他们(每个线程一个实例)。不过,这将涉及对要实施的代码进行潜在的昂贵更改。
您还需要查看 Fortran 代码的其他线程安全问题(这不是一个详尽的列表):
- 每个线程的 IO 单元应该是唯一的,否则文件 input/output 将不可靠
- 任何具有
SAVE
属性的变量(隐含在模块变量和声明时初始化的变量中)都是有问题的(这些变量在过程调用之间是持久的)。此属性的隐含性也 compiler/standard 相关,这使它成为一个更大的潜在问题。 - 使用
RECURSIVE
属性声明过程——这意味着该函数是可重入的。这也可以通过使用编译器 openmp 选项进行编译而不是更改代码来满足。
您可以探索的另一条路线是使用多处理或消息传递来并行化您的代码,而不是多线程。这避免了 Fortran 代码的线程安全问题,但带来了另一个可能代价高昂的代码架构更改。
另见:
是的,您不能将公共区域与多线程一起使用。不,没有办法避免这种情况。所有公共区域实际上都被链接器合并为单个块,并且无法在线程之间复制它。在存在遗留 Fortran 代码的任何地方,这都是一个已知问题。最常见的解决方案是使用多处理而不是多线程。
是的,公共块是共享的。
在 OpenMP 中,可以将公共块指定为 THREADPRIVATE。然后每个线程动态地创建公共块的新实例。要从原始数据复制数据,请使用 COPYIN 说明符。另见 Difference between OpenMP threadprivate and private
基本语法是
!$OMP THREADPRIVATE (/cb/, ...)
其中 cb 是公共块的名称。参见 https://computing.llnl.gov/tutorials/openMP/#THREADPRIVATE
感谢您的回答,特别是关于OpenMP的提示,确实可以。为了完全确定,我做了一个小程序。它由一个在一个主要 C++ 程序中调用的 Fortran 77 部分组成(这是我关心的):
fortran 77 例程func.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
包含文件 func.inc
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。
我无法捕捉到程序。相反,如果我在 func.inc 中删除 fortran 77 OMP 指令,那么公共块不再是私有的,并且会出现很多错误。
总而言之,我唯一需要做的就是在任何公共块后面添加 OMP 指令,希望这不会太复杂,因为它们都聚集在一个包含文件中(比如我的测试)。
希望这对您有所帮助。
此致。