这里我使用彼得森的算法来实现互斥。
我有两个非常简单的线程,一个用于将计数器增加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,但并非所有这些都在虚拟机中。有问题的计算机包含虚拟机和真机。
答案 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。
事实上,您可以将生产者/消费者简化为一个衬里,只需添加一个或减少一个衬里,它最终仍然会失败。我假设这些是一行功能标志。
考虑可能发生的事情:
制片人开始。这是一行counter++
。从低级别来看,这是什么?你可以想象像:
我并不是说这是确切的ASM,但可能会接近。
消费者恰好在生产者位置导致注册后获得上下文(开始运行)。堆栈窗台顶部为0,因此减少1 - 您得到-1
。
将继续使用之前的结果1
,并推送它。
现在你搞砸了。推回结果的最后一个线程将推送一个非零值,然后你就搞砸了。主要的谬误是假设++
是原子动作,这意味着它不能在中途停止。这里的墨菲定律是一个线程可以停止并切换到你的任何地方 - 它会.-
如果两个线程在不同的核心上,情况会更糟 - 他们可能会同时查看最初的0 ,这会让事情变得更加困难。再次,在并行编程中假设cpu想要搞砸你。
两个经典解决方案是使用互斥锁,请参阅Mutex example / tutorial?,或确保您认为是原子的行,实际上是 - http://en.cppreference.com/w/cpp/atomic/atomic(c ++)。
要明确的是,无论Peterson算法的并行工作如何,在系统正确实现线程的情况下如果++
本来就是原子的,那么你写的就永远不会像预期的那样停止。