Search code examples
clockingopenmp

How to use locks in sections correctly way in OpenMP?


I must parallelize the following code with 2 locks and output should be in order:
Hello
World
Bye
But when I run the threads are randomly doing the work.

#include <omp.h>
#include <stdio.h>

int main()
{
    int p;
    omp_lock_t lock;
    omp_init_lock(&lock);
    #pragma omp parallel sections default(shared)
    {
        
        #pragma omp section
        {
            p = omp_get_thread_num();
            omp_set_lock(&lock);
            printf("Th%d: Hello\n",p);
            omp_unset_lock(&lock);
        }

        #pragma omp section
        {   
            p = omp_get_thread_num();
            omp_set_lock(&lock);
            printf("Th%d: World\n",p);
            omp_unset_lock(&lock);
        }

        #pragma omp section
        {
            p = omp_get_thread_num();
            printf("Th%d: Bye\n",p);
        }
    }
    omp_destroy_lock(&lock);
    return 0;
}

Solution

  • From my top comments:

    Your lock does not guarantee ordering of your threads--only atomic access to stdout.

    So, for the latter, (e.g.) you'll always get "whole line" output:

    Hello\n
    World\n
    Bye\n
    

    And, not:

    HeWoByllo\ne\nrld\n
    

    But, with that you can get:

    World\n
    Bye\n
    Hello\n
    

    Also, does default(shared) mean that p is shared? I think p needs to be private [or needs to be set inside the critical section].

    Further, the 3rd section doesn't do any locking.

    I just ran the program, and got Th7 on all messages, so [AFAICT], p does need to be private.


    One way to solve this is with a "ticket lock" [*].

    Edit: After rereading your question, the emphasis on two locks may indicate an alternate solution, which I added in the UPDATE below.

    Because we can't predict what p will be, each section needs a sequence number.

    For example, Hello needs 1, World needs 2, and Bye needs 3.

    We need to add a shared variable that specifies which section may go next. Each section must loop on that variable [under lock] until it matches its assigned order/sequence number and increment it on the way out to allow the thread that is next in the sequence to run.

    [*] A "ticket lock" is modelled on (e.g.) a bakery where you "take a number" when entering and buy stuff when the sign says: "Now serving ..." See: https://en.wikipedia.org/wiki/Ticket_lock

    In general, one of the nice things about a ticket lock is that it guarantees progress and balanced access. In practice, the number of retries is relatively small/infrequent. This is particularly true if it's implemented using stdatomic.h primitives.

    Anyway, here's what I came up with:

    #include <omp.h>
    #include <stdio.h>
    
    int main()
    {
        int p;
        int owner = 1;
        int more = 1;
    
        omp_lock_t lock;
        omp_init_lock(&lock);
    
        #pragma omp parallel sections default(shared) private(p) private(more)
        {
    
            #pragma omp section
            {
                more = 1;
                while (more) {
                    p = omp_get_thread_num();
                    omp_set_lock(&lock);
    
                    if (owner == 1) {
                        printf("Th%d: Hello\n",p);
                        more = 0;
                        owner += 1;
                    }
    
                    omp_unset_lock(&lock);
                }
            }
    
            #pragma omp section
            {
                more = 1;
                while (more) {
                    p = omp_get_thread_num();
                    omp_set_lock(&lock);
    
                    if (owner == 2) {
                        printf("Th%d: World\n",p);
                        more = 0;
                        owner += 1;
                    }
    
                    omp_unset_lock(&lock);
                }
            }
    
            #pragma omp section
            {
                more = 1;
                while (more) {
                    p = omp_get_thread_num();
                    omp_set_lock(&lock);
    
                    if (owner == 3) {
                        printf("Th%d: Bye\n",p);
                        more = 0;
                        owner += 1;
                    }
    
                    omp_unset_lock(&lock);
                }
            }
        }
    
        omp_destroy_lock(&lock);
    
        return 0;
    }
    

    Here's the program output. The p values can vary from run to run, but now, the ordering is always the same:

    Th1: Hello
    Th7: World
    Th4: Bye
    

    UPDATE:

    The above solution is general [and I've used it a number of times in real production code], but it may not fit the requirement that you use two locks.

    What I can think of is to have two locks that are initialized and locked by the main thread before entering any critical sections.

    The first section (Hello) needs no lock and just goes first. The other two sections (World and Bye) will first lock on lock2 and lock3 respectively.

    When Hello finishes, it unlocks lock2.

    This allows World to get the lock and run. When finished, it unlocks both lock2 and lock3.

    The unlock of lock3 allows Bye to get that lock. It prints and then unlocks lock3

    Here's what I came up with for that:

    #include <omp.h>
    #include <stdio.h>
    
    int main()
    {
        int p;
    
        omp_lock_t lock2;
        omp_init_lock(&lock2);
        omp_set_lock(&lock2);
    
        omp_lock_t lock3;
        omp_init_lock(&lock3);
        omp_set_lock(&lock3);
    
        #pragma omp parallel sections default(shared) private(p)
        {
            #pragma omp section
            {
                p = omp_get_thread_num();
    
                printf("Th%d: Hello\n",p);
    
                omp_unset_lock(&lock2);
            }
    
            #pragma omp section
            {
                p = omp_get_thread_num();
                omp_set_lock(&lock2);
    
                printf("Th%d: World\n",p);
    
                omp_unset_lock(&lock2);
                omp_unset_lock(&lock3);
            }
    
            #pragma omp section
            {
                p = omp_get_thread_num();
                omp_set_lock(&lock3);
    
                printf("Th%d: Bye\n",p);
    
                omp_unset_lock(&lock3);
            }
        }
    
        omp_destroy_lock(&lock2);
        omp_destroy_lock(&lock3);
    
        return 0;
    }