我不知道这是否在任何地方被记录下来,如果有的话,我希望能有一个参考,但是我发现在使用OpenMP时出现了一些意外的行为。下面我有一个简单的程序来说明这个问题。在这里,我将以点的形式告诉你我希望程序做什么:
- 我想有2个线程
- 它们共享一个整数
- 第一个线程递增整数
- 第二个线程读取整数
- 递增一次后,外部进程必须告知第一个线程继续递增(通过互斥锁)
- 第二个线程负责解锁该互斥锁
正如您将看到的,线程之间共享的计数器没有为第二个线程正确更改。但是,如果我将计数器改为整数引用,我将得到预期的结果。下面是一个简单的代码示例:
#include <mutex>
#include <thread>
#include <chrono>
#include <iostream>
#include <omp.h>
using namespace std;
using std::this_thread::sleep_for;
using std::chrono::milliseconds;
const int sleep_amount = 2000;
int main() {
int counter = 0; // if I comment this and uncomment the 2 lines below, I get the expected results
/* int c = 0; */
/* int &counter = c; */
omp_lock_t mut;
omp_init_lock(&mut);
int counter_1, counter_2;
#pragma omp parallel
#pragma omp single
{
#pragma omp task default(shared)
// The first task just increments the counter 3 times
{
while (counter < 3) {
omp_set_lock(&mut);
counter += 1;
cout << "increasing: " << counter << endl;
}
}
#pragma omp task default(shared)
{
sleep_for(milliseconds(sleep_amount));
// While sleeping, counter is increased to 1 in the first task
counter_1 = counter;
cout << "counter_1: " << counter << endl;
omp_unset_lock(&mut);
sleep_for(milliseconds(sleep_amount));
// While sleeping, counter is increased to 2 in the first task
counter_2 = counter;
cout << "counter_2: " << counter << endl;
omp_unset_lock(&mut);
// Release one last time to increment the counter to 3
}
}
omp_destroy_lock(&mut);
cout << "expected: 1, actual: " << counter_1 << endl;
cout << "expected: 2, actual: " << counter_2 << endl;
cout << "expected: 3, actual: " << counter << endl;
}
以下是我的输出:
increasing: 1
counter_1: 0
increasing: 2
counter_2: 0
increasing: 3
expected: 1, actual: 0
expected: 2, actual: 0
expected: 3, actual: 3
gcc版本:9.4.0
其他发现:
- 如果我使用OpenMP的“section”而不是“tasks”,我也会得到预期的结果。
- 如果我使用posix信号量,这个问题也会继续存在。
1条答案
按热度按时间hgb9j2n61#
这是不允许从另一个线程解锁互斥体的。这样做会导致未定义的行为。在这种情况下,一般的解决方案是使用信号量。等待条件也会有所帮助(关于真实世界的用例)。引用OpenMP documentation(注意,几乎所有互斥体实现都共享此约束,包括pthreads):
如果某个程序访问的锁不处于锁定状态,或者不属于包含通过任一例程进行的调用的任务,则该程序是不相容的。
通过任一例程访问未处于未初始化状态的锁的程序是不相容的。
此外,这两个任务可以在同一线程或不同线程上执行。您不应对它们的调度作任何假设,除非您告诉OpenMP这样做具有依赖性。在这里,运行时串行执行任务是完全符合要求的。您需要使用OpenMP段,以便多个线程执行不同的段。此外,在任务中使用锁通常被认为是不好的做法,因为运行时调度程序并不知道它们。
最后,在这种情况下不需要锁:一个原子操作就足够了。幸运的是,OpenMP supports atomic operations(以及C++)。
附加注解
请注意,由于内存屏障,锁定可保证多个线程中内存访问的一致性。实际上,互斥锁上的解锁操作会导致释放内存屏障,使写入操作对其他线程可见。来自另一个线程的锁定会导致获取内存屏障,强制读取操作在锁定后完成。如果未正确使用锁定/解锁,内存访问的方式不再安全,这会导致一些变量无法从其他线程更新。更普遍地说,这也会产生竞争条件。因此,简单地说,不要这样做。