Search code examples
openmp

When to use the atomic clause and single worksharing construct in openMP programs?


I'm just beginning to learn to use OpenMP. I'm curious about when one would use the single work sharing construct and when one would use the atomic clause.

From what I've read so far, the atomic clause seems redundant. The single construct denotes a block of code that is executed by only one thread (not necessarily the master thread) and the atomic clause provides mutual exclusion but only applies to the update of a memory location (the update of X in the following example).

Despite the extra condition placed on the atomic clause (that it can only be used in update situations), it seems like the single construct is capable of achieving everything the atomic clause can.

I know I'm missing something, can someone please motivate the existence of the atomic clause ?


Solution

  • The two directives are completely different.

    single does not provide any mechanism related to mutual exclusion or instruction atomic. As a result, a single section can work on a shared variable modified by other threads. Your are confused because you probably assumes that others threads are not doing anything during a single section. This is wrong. Indeed, threads could finish a parallel for (or another single section) with a nowait close or could also create OpenMP tasks executed during the single section, etc. Here is an example:

    #pragma omp parallel
    {
        ...
    
        // This section does not wait for others threads
        #pragma omp master
        {
            // some computation
            int a = rand();
            // some computation
        }
    
        // This section wait for others threads only at the end
        #pragma omp single
        {
            // some computation
            int b = rand();
            // some computation
        }
    
        ...
    }
    

    In the code above, the two rand calls can be executed in parallel (because the master section can be executed by one thread while another thread run the single section). Since rand is not thread safe, this can cause undefined results (probably a race condition). Moreover, please note that using a single directive does not make the rand call atomic.

    In order to avoid race conditions, you can use synchronization directives such as barrier or taskwait. You can also use mutual exclusion using critical or atomic instructions using atomic. Synchronizations directives should be avoided (when it is possible) as they often cause scalability issues. atomic instructions can only be applied on specific cases. For example, you cannot use atomic directives on the rand calls in the code above. However, you can use critical sections to protect these calls (while keeping the single and master sections that can be executed in parallel).

    atomic should be preferred over critical when both make sense because the overhead of atomic instructions is generally smaller. An atomic directive cannot be replaced by only a single directive because the atomic is performed by each thread of the parallel section while the single section is executed by only one.