Multi Threading - Peterson's algorithm not working

Viewed 880

Here I use Peterson's algorithm to implement mutual exclusion.

I have two very simple threads, one to increase a counter by 1, another to reduce it by 1.

const int PRODUCER = 0,CONSUMER =1;
int counter;
int flag[2];
int turn;

void *producer(void *param)
{

    flag[PRODUCER]=1;
    turn=CONSUMER;
    while(flag[CONSUMER] && turn==CONSUMER);

    counter++;

    flag[PRODUCER]=0;
}

void *consumer(void *param)
{
    flag[CONSUMER]=1;
    turn=PRODUCER;
    while(flag[PRODUCER] && turn==PRODUCER);

    counter--;

    flag[CONSUMER]=0;
}

They works fine when I just run them once.

But when I run them again again in a loop, strange things happen.

Here is my main function.

int main(int argc, char *argv[])
{
    int case_count =0;
    counter =0;
    while(counter==0)
    {
        printf("Case: %d\n",case_count++);
        pthread_t tid[2];
        pthread_attr_t attr[2];

        pthread_attr_init(&attr[0]);
        pthread_attr_init(&attr[1]);

        counter=0;
        flag[0]=0;
        flag[1]=0;
        turn = 0;

        printf ("Counter is intially set to %d\n",counter);

        pthread_create(&tid[0],&attr[0],producer,NULL);
        pthread_create(&tid[1],&attr[1],consumer,NULL);

        pthread_join(tid[0],NULL);
        pthread_join(tid[1],NULL);

        printf ("counter is now %d\n",counter);
    }

    return 0;
}

I run the two threads again and again, until in one case the counter isn't zero.

Then, after several cases, the program will always stop! Some times after hundreds of cases, some times thousands, or event tens of thousand.

It means in one case the counter isn't zero. But why??? the two threads modify the counter in critical session, and increase and decrease it only once. Why will the counter not be zero?

Then I run this code in other computers, more strange things happen - in some computers the program seems has no problem, and the others have the same problem with me! Why?

By the way, in my computer, I run this code in VM ware's virtual computer, Ubuntu 16.04. Others' computer is also Ubuntu 16.04, but not all of them are in virtual machines. And the computer with problem contains both virtual machines and real machines.

2 Answers

You need hardware support to implement any kind of thread-safe algorithm.

There are many reasons why your code is not working at you intended. The simplest one is that the cores have individual caches. So your program starts on say two cores. Both cache flag to be 0, 0. They both modify their own copy, so they don't see what the other core is doing.

In addition memory works in blocks, so writing flag[PRODUCER] will very likely write flag[CONSUMER] as well (because ints are 4 bytes and most of todays processors have memory blocks of 64 bytes).

Another problem would be operation reordering. Both the compiler and the processor are allowed to swap instructions. There are constraints that dictate that the single threaded execution result shouldn't change, but obviously they don't apply here.

The compiler might also figure out that you are setting turn to x and then checking if it is x, which is obviously true in a single threaded world so it can be optimized away.

This list is not exhaustive. There are many more things (some platform specific) that could happen and break your program.

So, at the very least try to use std::atomic types with strong memory ordering (memory_order_seq_cst). All your variables should be std::atomic. This gives you hardware support but it will be a lot slower.

This will still not work because most you might still have some piece of code where you read and then change. This is not atomic because some other thread might have changed the data after your read and before you changed it.

Peterson's algorithm only works on single core processors/single CPU systems.

That's because they don't do real parallel processing. Two atomar operations never get executet at the same time there.

If you got 2 or more CPUs/CPU cores the amount of atomar operations who can be executed at the same time increase by one for each cpu(core). This means, even if an integer assignment is atomar it can be executed multiple times at the same time in different CPUs/Cores.

In your case turn=CONSUMER/PRODUCER; is just called twice at the same time in different CPUs/cores.

Deacitvate all CPU cores but one for your program and it should work fine.

Related