多线程 - 彼得森的算法不起作用

时间:2017-11-23 13:17:53

标签: c++ c multithreading algorithm

这里我使用彼得森的算法来实现互斥。

我有两个非常简单的线程,一个用于将计数器增加1,另一个用于将计数器减少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;
}

当我只运行一次时它们工作正常。

但是当我再次在循环中再次运行它们时,会发生奇怪的事情。

这是我的main功能。

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;
}

我一次又一次地运行两个线程,直到一个案例中计数器不为零。

然后,在几个案例之后,程序将永远停止!有数百次,有时数千次,或事件数万次。

这意味着在一种情况下,计数器不为零。但为什么???两个线程在关键会话中修改计数器,并且只增加和减少一次。为什么计数器不是零?

然后我在其他计算机上运行此代码,发生更奇怪的事情 - 在某些计算机上,程序似乎没有问题,其他人也有同样的问题!为什么?

顺便说一句,在我的计算机中,我在VM ware的虚拟计算机Ubuntu 16.04中运行此代码。其他'计算机也是Ubuntu 16.04,但并非所有这些都在虚拟机中。有问题的计算机包含虚拟机和真机。

3 个答案:

答案 0 :(得分:2)

您需要硬件支持才能实现任何类型的线程安全算法。

您的代码无法正常运行的原因有很多。最简单的一个是内核具有单独的缓存。所以你的程序开始说两个核心。缓存标志都是0,0。它们都修改了自己的副本,所以他们不知道其他核心在做什么。

此外,内存工作在块中,因此写入标志[PRODUCER]也很可能会写入标志[CONSUMER](因为整数是4个字节,而今天的大多数处理器都有64字节的内存块)。

另一个问题是操作重新排序。允许编译器和处理器交换指令。有些约束要求单线程执行结果不应该改变,但显然它们不适用于此。

编译器也可能会发现你正在设置转向x,然后检查它是否为x,这在单线程世界中显然是正确的,因此可以对其进行优化。

此列表并非详尽无遗。还有很多事情(某些特定于平台)会发生并破坏你的程序。

因此,至少尝试使用具有强内存排序的std :: atomic类型(memory_order_seq_cst)。你的所有变量都应该是std :: atomic。这为您提供硬件支持,但速度会慢很多。

这仍然不起作用,因为大多数人可能仍然有一些代码,您可以在其中阅读然后进行更改。这不是原子的,因为其他一些线程可能在您读取之后和更改之前更改了数据。

答案 1 :(得分:2)

Peterson的算法仅适用于单核处理器/单CPU系统。

那是因为他们没有进行真正的并行处理。两个atomar操作永远不会在那里同时执行。

如果你有两个或更多的CPU / CPU核心,那么每个cpu(核心)可以同时执行的atomar操作量会增加一个。 这意味着,即使整数赋值是atomar,它也可以在不同的CPU /核心中同时执行多次。

在您的情况下,turn=CONSUMER/PRODUCER;在不同的CPU /核心中同时被调用两次。

取消所有CPU核心,但一个用于您的程序,它应该可以正常工作。

答案 2 :(得分:0)

我想给出一个答案,以阐明评论中的条款,例如" atomic"行动和竞争条件。这似乎是合理的,因为你在一个进程中只添加一个,在第二个进程中减少一个,你应该总是得到0。

事实上,您可以将生产者/消费者简化为一个衬里,只需添加一个或减少一个衬里,它最终仍然会失败。我假设这些是一行功能标志。

考虑可能发生的事情:

  1. 制片人开始。这是一行counter++。从低级别来看,这是什么?你可以想象像:

    • 将1添加到堆栈顶部并将结果放入寄存器。 (可能是两个周期?也许是另一个注册?只是一个例子。)
    • pop stack。
    • 将寄存器推送到堆栈。

    我并不是说这是确切的ASM,但可能会接近。

  2. 消费者恰好在生产者位置导致注册后获得上下文(开始运行)。堆栈窗台顶部为0,因此减少1 - 您得到-1

  3. 将继续使用之前的结果1,并推送它。

  4. 现在你搞砸了。推回结果的最后一个线程将推送一个非零值,然后你就搞砸了。主要的谬误是假设++是原子动作,这意味着它不能在中途停止。这里的墨菲定律是一个线程可以停止并切换到你的任何地方 - 它会.-

    如果两个线程在不同的核心上,情况会更糟 - 他们可能会同时查看最初的0 ,这会让事情变得更加困难。再次,在并行编程中假设cpu想要搞砸你。

    两个经典解决方案是使用互斥锁,请参阅Mutex example / tutorial?,或确保您认为是原子的行,实际上是 - http://en.cppreference.com/w/cpp/atomic/atomic(c ++)。

    要明确的是,无论Peterson算法的并行工作如何,在系统正确实现线程的情况下如果++本来就是原子的,那么你写的就永远不会像预期的那样停止。