Exceptions in OpenMP and C++ – what’s the state of affairs today?
As you may know from reading one of my last posts (e.g. the one regarding Scoped Locking in OpenMP), I am interested in issues regarding C++ and OpenMP. One of these is how to make exceptions work with OpenMP. In this article I am going to highlight the present state of affairs regarding exceptions in OpenMP. In a followup article, I will sketch some limited ways to make exceptions work with certain OpenMP-constructs and further comment on why these ways will only take you this far.
All you want to do is program in OpenMP and C++, no need for exceptions in your code, you say? Well, take a look at this innocent looking piece of code:
- #pragma omp parallel
- {
- // do something useful
- ...
- some_var = new TYPE;
- ...
- // do more useful stuff
- }
Looks familiar, doesn’t it? I don’t know how often I have written OpenMP-code that looked like this in the past and never worried about exceptions. Yet, the new-expression calls the operator new, which in turn is able to throw an exception of type std::bad_alloc (at least if you do not pass std::nothrow as placement parameter, which I admit to do not very often). Exceptions in C++ are not as common as e.g. in Java, but they are definitely there and I therefore think its worth exploring how they interact with my parallel programming system of choice.
Enough of the motivation, let’s start by figuring out what the OpenMP specification has to say about exceptions:
A throw executed inside a parallel region must cause execution to resume within the same parallel region, and it must be caught by the same thread that threw the exception.
This basically means that exceptions are not allowed to cross thread-boundaries, which makes sense. If they were allowed to do so, this could lead to situations where two exceptions are active in the same thread at the same time, which is forbidden by the C++-standard (actually, the C++-standard does not even mention threads as of now, but as far as I know forbids two exceptions to be active at the same time). So we cannot propagate exceptions to the master thread at the end of parallel regions and it therefore makes sense to require them to be caught before the end of the parallel region. The only other way I can think of to handle this situation is the Java way: as far as I know, if an exception is not caught when a thread ends its lifetime in Java, it is just dropped. This may work for Java, where exceptions are a lot more common than in C++. In C++ it is a recommended practice to use exceptions only to signal serious error conditions and it is probably not a good idea to hide these errors by silently dropping exceptions. For the reasons presented, I am quite happy with this part of the specification.
Let’s see what else we can find. The next interesting quote can be spotted in the definition of a structured block:
For C/C++, an executable statement, possibly compound, with a single entry at the top and a single exit at the bottom. […] longjmp() and throw() must not violate the entry/exit criteria.
This basically means that it is not allowed to throw an exception out of a structured block. It is allowed to throw an exception inside a structured block if it is caught inside that same block, though! Why is this restriction so important? Well, basically because every OpenMP-directive has an associated structured block. And this in turn means, you cannot throw an exception out of any work-sharing region (e.g. for, sections), critical region or master region. Which is quite a restriction, really. Remember that you can throw an exception inside these regions, if you catch it inside that very same region, though, which makes exceptions not totally useless in OpenMP.
There is a third point to keep in mind when talking about exceptions and OpenMP, and it is barriers. Consider the following small code example, slightly adapted from one of my test-cases:
- #pragma omp parallel num_threads (numthreads)
- {
- try {
- if (omp_get_thread_num () == 0) {
- throw 0;
- }
- #pragma omp barrier
- }
- catch(int e) {
- /* do something useful */
- }
- }
Please ignore the fact for now that the code does not do anything useful. It illustrates that exceptions and barriers do not mix too well, though, if you are not really careful. Technically, the code in question does not violate the two parts of the specification that I have cited earlier: the exception is caught in the same thread and no structured block spawned by any OpenMP-construct is violated. Still, this code will most likely deadlock, because all of the threads except the master will end up waiting in the explicit barrier, while the master waits in the closing barrier at the end of the parallel region. Of course, this is quite easy to spot and is non-conforming code anyways, but just imagine what happens when a function way deeper down inside the call stack throws an exception (e.g. the new expression). The mistake becomes quite hard to spot in this case.
To sum up this short article, let me try to recap the points I am trying to make here:
- even though C++ does not use exceptions excessively, they are used, sometimes even in innocent-looking code (e.g. the new-expression)
- exceptions and OpenMP do work together in principle, but there are many restrictions that limit their usefulness considerably
- all exceptions need to be caught before the end of the parallel region, or else the code is not conforming
- no exceptions can be thrown out of a structured block associated with most OpenMP-constructs; if an exception is thrown inside such a structured block it must be caught before the block is left
- barriers and exceptions don’t play well together
So much for the state of the art regarding exceptions, OpenMP and C++ today, hope I was able to tell you something new ;).
[Update]: I have written a follow-up article on this topic here.