OpenMP是否禁止在并行区域内抛出异常,即使不运行多线程?

huangapple go评论48阅读模式
英文:

does OpenMP prohibit a throw inside a parallel region even when not running multithreaded?

问题

在并行处理中,如果使用了#pragma omp critical指令,即使在单线程模式下,抛出异常也是不允许的。这可能导致你在单线程模式下遇到问题。为了避免这种情况,你可以考虑在检测到不需要并行化时,避免使用#pragma omp critical。例如,你可以修改你的代码,避免在非并行情况下使用#pragma omp critical,如下所示:

if (will_parallelize) {
    #pragma omp parallel default(none) shared(...)
    {
        ...
        #pragma omp for schedule(dynamic, 1)
        for (...) {
            ...
            // Perform operations without critical section when parallelizing
            if (some_condition) {
                // Code without critical section
            } else {
                // Code that might raise an exception without critical section
            }
        }
    }
} else {
    // Code without parallelization, handle critical section differently if needed
    for (...) {
        ...
        // Handle critical section without using #pragma omp critical
        if (some_condition) {
            // Code inside critical section
        } else {
            // Code that might raise an exception without critical section
        }
    }
}

请注意,上述示例代码中,当不需要并行化时,避免使用#pragma omp critical,而是在代码中使用条件语句来处理可能引发异常的部分。这样,你可以在单线程和多线程模式下都避免抛出异常,同时避免了写入大量重复代码。

英文:

First of all, yes, I am aware of answers such as this on SO:

https://stackoverflow.com/q/13663231/2752221

But I don't think it answers my specific question. Here's my situation. I have a loop that I want to parallelize, and for reasons not important here, it is done with #pragma omp parallel followed by #pragma omp for, rather than with #pragma omp parallel for, like so:

#pragma omp parallel default(none) shared(...) if(will_parallelize)
{
	...
								
#pragma omp for schedule(dynamic, 1)
	for (...)
	{

I've elided unimportant details with ellipses, .... The only important thing about the specifics of the above code is the if(will_parallelize) on the #pragma omp parallel directive. We'll return to that in a moment.

Inside the loop, various functions get called, and inside that call tree, at some point there is a critical section, and inside that critical section is code that can raise:

#pragma omp critical (MyName)
{
	... stuff that might raise ...
}

Here's the key point: outside of the parallel region, my code can determine whether or not a raise is possible. If it is possible, I set will_parallelize to false, and so the whole thing runs single-threaded. If no raise is possible, then will_parallelize gets set to true, and the loop runs in parallel.

Here's the problem: when will_parallelize is false, if a raise occurs my program gets terminated with the error:

libc++abi: terminating due to uncaught exception of type std::runtime_error

My understanding was that throwing was only illegal when running parallel, and that if I avoid running parallel, as with my if() above, I can throw. I was expecting the critical section to be a no-op when running single-threaded, such that throwing from within it is OK when running single-threaded. But apparently not...? Or have I confused myself, and something else is actually going on?

If I'm not allowed to raise when I'm inside a parallel region even when my if() test tells it not to run in parallel, what's the recommended fix here? It feels like I would have to actually copy/paste the whole chunk of code with the for loop, and test will_parallelize myself and avoid the #pragma omp parallel altogether, like so:

if (will_parallelize)
{
#pragma omp parallel default(none) shared(...)
	{
		...
								
#pragma omp for schedule(dynamic, 1)
		for (...)
		{
			...
		}
	}
}
else
{
	... DUPLICATED CODE ...
	
	for (...)
	{
		... DUPLICATED CODE ...
	}
}

This is rather awful. But I'm not sure even this would fix the problem, if I'm not allowed to raise when inside an OpenMP construct like #pragma omp critical even when running single-threaded, because remember, the raise is occurring inside a critical region that is deep down in nested function calls. So maybe even doing the above fix wouldn't suffice; maybe I need to do something like this, in that nested function:

if (omp_in_parallel())
{
#pragma omp critical (MyName)
	{
		... stuff that does not raise when we choose to run parallel ...
	}
}
else
{
	... DUPLICATED CODE that might raise when single-threaded ...
}

This is, again, rather awful. I thought the whole design of OpenMP was intended to make it so that the same code would run both single-threaded and multithreaded; but in this case, it seems like the design is not ensuring that, and so I'm fighting OpenMP. Probably my fault. What am I doing wrong?

If it matters, this is on macOS 13.3.1, Xcode 14.3, with whatever version of Apple Clang that implies; I'm not going to tag this question with any of that, though, since my question is really about what the OpenMP spec does and does not allow, and how to work within the parameters of what is allowed without writing ugly duplicated code.

答案1

得分: 1

OpenMP可以根据是否有omp而进行编译,可以选择是否使用线程。但一旦编译完成,它就被设置好了。

一个解决方案可能是“中断”而不是抛出:

设置另一个变量来跟踪一个原因(如果需要的话),将循环变量设置为满足结束条件,然后继续循环以中断。

英文:

OpenMP is able to be compiled with or without threading depending on whether or not you have omp, but once it's compiled it is set.

A solution could be to "break" rather than throw:

set another variable to track a reason (if you want), set the loop variable to meet the end condition, then continue the loop to break.

int main(int argc,char **argv) {
  bool will_parallelize = true;
  for (int i=0;i<2;++i) {
    if (i) will_parallelize=false;
    int failcode = 0;
    #pragma omp parallel if(will_parallelize)
    {
      #pragma omp for schedule(static, 1)
      for (int x=0;x<20;++x) {
        if (i) { failcode=1; x=19; continue; }
        const int tid = omp_get_thread_num();
        printf("%d:%d\n",tid,x);
      }
      // close off any other place if you want to do more
      if (!failcode) {
        ...
      }
    }
    if (failcode) printf("Error set\n");
  }
}

答案2

得分: 1

你可以尝试使用 omp cancel 构造,但可能会减慢执行速度。另外,如果我记得正确的话,它需要启用特殊的环境变量(OMP_CANCELLATION)。

英文:

Also you can try omp cancel construct, but it may slowdown the execution. Also, IIRC it requires turning on special environment variable (OMP_CANCELLATION)

答案3

得分: 0

我不满意我最终的结果,但据我所知,这是我最好的选择,所以我将它发布在这里。

首先,事实证明,无论该区域是否是“活跃的”(使用多个线程)或“非活跃的”(仅使用一个线程),你都不能在并行区域内抛出异常(但请参见下文以获得更准确的说明)。对于关键区域,据我所知,规则更为严格——你根本不能在关键区域内抛出异常,无论你是否根本不在并行区域内(无论是活跃还是非活跃)。这些限制似乎出人意料地严格。但我知道什么,我不制定规则,这些规则可能有很好的理由。

无论如何,我刚刚写的并不完全正确——你确实可以抛出异常,只是它不能越过你所在的结构(并行区域、关键区域等)。你需要在相同的结构内捕获它。

基于这些规则,怎么办?我最终的代码看起来像这样:

#ifdef _OPENMP
bool saw_error_in_critical = false;
#endif

#pragma omp critical (MyName)
{
try {
... 做可能引发异常的事情 ...
} catch (...) {
#ifdef _OPENMP
saw_error_in_critical = true;
#else
throw;
#endif
}
}

#ifdef _OPENMP
if (saw_error_in_critical)
{
... 引发新异常 ...
}
#endif

因此,当我的程序在没有 OpenMP 编译时,它只是重新抛出发生的任何异常。事实上,在这种情况下,我可以完全摆脱 try/catch。这些异常很少发生,所以这并不重要,而且这种方式似乎涉及的预处理器混乱略少一些。当程序在使用 OpenMP 编译时,它会捕获异常,设置一个标志,并通过代码的自然流程退出关键区域。一旦离开关键区域,它就会抛出一个新异常。如果能抛出相同的异常就好了,但我还没有找到如何做到这一点——事实上,根据 C++ 规范,我认为你不能,所以我只是抛出一个新异常而已。

我对我的并行区域做同样的事情——try/catch,如果在 OpenMP 下就设置一个标志,并在标志被设置时在并行区域外再次抛出。这不太美观,我希望 OpenMP 规范对异常不要那么严格,但我想他们也有他们的理由。这个解决方案似乎有效,据我所知。它让我保留了代码的总体设计(在深度嵌套函数内部的异常条件偶尔会引发)而无需进行大规模重构,也不违反 OpenMP 的规则。我可能可以为这些事情定义宏,让代码看起来更漂亮,但老实说,我更喜欢看到丑陋的内部,这样我就可以清楚地看到发生了什么。:->

可能存在一种潜在的竞争,我认为,一个线程可能会完成关键区域,然后在重新抛出之前,另一个线程可能会进入并开始执行关键区域,可能会使用之前线程留下的(可能是)糟糕状态。对于我的代码,我不需要担心这个;但我可以想象,对于其他一些代码来说,这可能是个问题,所以_慎重阅读_。你可能可以使用一个静态/全局锁或类似的东西,以防止另一个线程在前一个线程放弃锁但没有重新抛出时进入关键区域。在那时,我想也许你甚至不需要关键区域,锁正在做很多相同的事情——尽管没有障碍。

哦,顺便说一句:如果你违反规则,在这样的区域内抛出异常,而不在区域内捕获异常,至少在我的平台上,你会得到一个“未捕获的 C++ 异常”运行时错误。这很好——你不仅会得到一般意义上的“未定义行为”,而且你会得到一个你可以在调试器中中断的东西。这非常有帮助。

英文:

I'm not happy with where I ended up, but as far as I can tell, it's the best option I have, so I'm posting it here.

First of all, it does turn out that you can't throw an exception inside a parallel region whether the region is "active" (using more than one thread) or "inactive" (using only one thread) (but see below more a more precise statement). For critical regions, as far as I can tell, the rules are even more strict – you can't throw an exception inside a critical region at all, period, even if you are not inside a parallel region at all (whether active or inactive). These restrictions seem surprisingly... restrictive. But what do I know, I don't make the rules and there are probably good reasons for them.

In any case, what I just wrote is not quite right – you can throw an exception, it just can't cross outside of the construct you're in (parallel region, critical region, etc.). You need to catch it inside the same construct.

Given those rules, what to do? Where I ended up looks like this:

#ifdef _OPENMP
bool saw_error_in_critical = false;
#endif

#pragma omp critical (MyName)
{
	try {
		... do stuff that might raise ...
	} catch (...) {
#ifdef _OPENMP
		saw_error_in_critical = true;
#else
		throw;
#endif
	}
}

#ifdef _OPENMP
if (saw_error_in_critical)
{
	... raise a new exception ...
}
#endif

So when my program is compiled without OpenMP, it just rethrows any exception that occurs. I could get rid of the try/catch altogether in that case, in fact. These exceptions are rare, so it doesn't really matter, and this way of doing seemed to involve slightly less preprocessor ugliness. When the program is compiled with OpenMP, it catches the exception, sets a flag, and exits the critical region by the natural flow of the code. Once outside the critical region, it throws a new exception. It would be nice if I could throw the same exception, but I haven't figured out how to do that – I think you can't, according to the C++ spec, in fact – so I just throw a new one instead.

I do the same thing for my parallel regions – try/catch, set a flag if we're under OpenMP, and throw again outside the parallel region if the flag is set. It's not pretty, and I wish the OpenMP spec weren't quite so strict about exceptions, but I suppose they have their reasons. This solution seems to work, AFAICT. It lets me retain the overall design of my code (where an exceptional condition inside a deeply nested function can occasionally raise) without massive restructuring, and without violating OpenMP's rules. I could probably define macros for these things that would let me make the code look prettier, but honestly I prefer to see the ugly guts so I can see what's happening explicitly. :->

There is a potential race of sorts, I think, in the sense that one thread could finish the critical region, and then before it gets to the re-throw, another thread comes along and starts executing the critical region, potentially doing stuff with the (presumably) bad state left behind by the previous thread after the exception occurred. For my code, I don't need to worry about that; but I can imagine that that might pose a problem for some other code, so caveat lector. You could probably use a static/global lock or some such, to prevent another thread from entering the critical region until the previous thread has let go of the lock without re-throwing. (At that point, I guess maybe you don't even need the critical region, the lock is doing much the same thing – although without a barrier.)

Oh, and by the way: if you break the rules, and throw inside such a region without catching inside the region, you get an "uncaught C++ exception" runtime error, at least on my platform. So that's nice – you don't just get "undefined behavior" in the general sense, you get something you can break on in the debugger. That was very helpful.

huangapple
  • 本文由 发表于 2023年5月14日 20:47:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/76247565.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定