彼得森的解决方案实施不适用于C语言

时间:2015-07-29 02:52:28

标签: c concurrency operating-system pthreads concurrent-programming

我有以下代码,我试图了解彼得森的解决方案。当我为小到9999的循环运行此实现时,输出正确地显示为0,但是当我使用更高的循环值(如9999999)测试时,我得到接近0但不是0的值,是否可能增加和递减线程可以在(*(a))++;部分执行吗?为什么以下实施不起作用?我的程序中有没有错误?

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>

#define LOOP 9999999

int interest[2] = {0,0};
int turn = 0;
int loops[2] = {LOOP,LOOP};

void increment(int *a) {
  printf("Incrementing %p\n",a);
  for(int i=0;i<LOOP;i++) {
    interest[0] = 1;
    turn = 1;
    while(interest[1] == 1 && turn == 1);
    (*(a))++;
    loops[0]--;
    interest[0] = 0;
  }
}

void decrement(int *a) {
  printf("Decrementing %p\n",a);
  for(int i=0;i<LOOP;i++) {
    interest[1] = 1;
    turn = 0;
    while(interest[0] == 1 && turn == 0);
    (*(a))--;
    loops[1]--;
    interest[1] = 0;
  }
}

void print_val(int *a) {
  while(1) {
    getchar();
    printf("value at mem %d\niInc(%d) iDec(%d)\n",*a,loops[0],loops[1]);
  }
}

int main() {

  pthread_t t1, t2, t3;
  int *mem = malloc(sizeof(int));

  pthread_create(&t1, NULL, (void*)decrement, (void*)mem);
  pthread_create(&t2, NULL, (void*)increment, (void*)mem);
  pthread_create(&t3, NULL, (void*)print_val, (void*)mem);

  pthread_join(t1,NULL);  
  pthread_join(t2,NULL);  
  printf("operation complete\n");
  pthread_join(t3,NULL);  

  return 0;
}

输出:

$:~/Arena/sem $ gcc op.c -pthread && ./a.out
Incrementing 0xd16010
Decrementing 0xd16010
operation complete

value at mem -2
iInc(0) iDec(0)
^Z
[13]+  Stopped                 ./a.out
$:~/Arena/sem $ gcc op.c -pthread && ./a.out
Decrementing 0x2432010
Incrementing 0x2432010
operation complete

value at mem 16
iInc(0) iDec(0)
^Z
[14]+  Stopped                 ./a.out
meow:~/Arena/sem $ 

编辑:

  1. 我已经尝试Wrong implementation of Peterson's algorithm?并且添加volatile没有帮助,我也没有像上一个帖子中所说的那样使用++操作。
  2. Implementation of Peterson's solution doesn't work properlyPeterson algorithm in Java?不在C中,我的帖子不是骗局。
  3. 查看答案:

    大多数答案都表明编译器可能会进行一些重新排序,我已经添加了汇编转储,有人可以帮助我理解这些进程如何最终进入关键部分吗?

    增量功能

     for(int i=0;i<LOOP;i++) {
      25:   c7 45 f4 00 00 00 00    mov    DWORD PTR [ebp-0xc],0x0
      2c:   eb 50                            jmp    7e <increment+0x7e>
        interest[0] = 1;
        turn = 1;
      2e:   c7 05 00 00 00 00 01   mov    DWORD PTR ds:0x0,0x1
      35:   00 00 00 
        while(interest[1] == 1 && turn == 1);
      38:   c7 05 00 00 00 00 01   mov    DWORD PTR ds:0x0,0x1
      3f:   00 00 00 
        //while(turn == 1 && interest[1] == 1);
      42:   90                                 nop
      43:   a1 04 00 00 00             mov    eax,ds:0x4
      48:   83 f8 01                        cmp    eax,0x1
      4b:   75 0a                            jne    57 <increment+0x57>
      4d:   a1 00 00 00 00             mov    eax,ds:0x0
      52:   83 f8 01                        cmp    eax,0x1
      55:   74 ec                             je     43 <increment+0x43>
        (*(a->location))++;
      57:   8b 45 08                        mov    eax,DWORD PTR [ebp+0x8]
      5a:   8b 00                             mov    eax,DWORD PTR [eax]
      5c:   8b 10                             mov    edx,DWORD PTR [eax]
      5e:   83 c2 01                        add    edx,0x1
      61:   89 10                             mov    DWORD PTR [eax],edx
        loops[0]--;
      63:   a1 00 00 00 00              mov    eax,ds:0x0
      68:   83 e8 01                        sub    eax,0x1
      6b:   a3 00 00 00 00              mov    ds:0x0,eax
        interest[0] = 0;
      70:   c7 05 00 00 00 00 00    mov    DWORD PTR ds:0x0,0x0
      77:   00 00 00 
    

    递减函数

    for(int i=0;i<LOOP;i++) {
      8f:   8b 45 08                       mov    eax,DWORD PTR [ebp+0x8]
      92:   8b 50 04                      mov    edx,DWORD PTR [eax+0x4]
      95:   8b 45 08                      mov    eax,DWORD PTR [ebp+0x8]
      98:   8b 00                           mov    eax,DWORD PTR [eax]
      9a:   89 54 24 08                 mov    DWORD PTR [esp+0x8],edx
      9e:   89 44 24 04                 mov    DWORD PTR [esp+0x4],eax
      a2:   c7 04 24 28 00 00 00  mov    DWORD PTR [esp],0x28
      a9:   e8 fc ff ff ff                 call   aa <decrement+0x21>
        interest[1] = 1;
      ae:   c7 45 f4 00 00 00 00   mov    DWORD PTR [ebp-0xc],0x0
      b5:   eb 4f                            jmp    106 <decrement+0x7d>
        turn = 0;
        while(interest[0] == 1 && turn == 0);
      b7:   c7 05 04 00 00 00 01  mov    DWORD PTR ds:0x4,0x1
      be:   00 00 00 
        //while(turn == 0 && interest[0] == 1);
      c1:   c7 05 00 00 00 00 00  mov    DWORD PTR ds:0x0,0x0
      c8:   00 00 00 
        (*(a->location))--;
      cb:   90                                nop
      cc:   a1 00 00 00 00            mov    eax,ds:0x0
      d1:   83 f8 01                      cmp    eax,0x1
      d4:   75 09                           jne    df <decrement+0x56>
      d6:   a1 00 00 00 00            mov    eax,ds:0x0
      db:   85 c0                           test   eax,eax
      dd:   74 ed                           je     cc <decrement+0x43>
        loops[1]--;
      df:   8b 45 08                       mov    eax,DWORD PTR [ebp+0x8]
      e2:   8b 00                            mov    eax,DWORD PTR [eax]
      e4:   8b 10                            mov    edx,DWORD PTR [eax]
      e6:   83 ea 01                       sub    edx,0x1
      e9:   89 10                            mov    DWORD PTR [eax],edx
        interest[1] = 0;
      eb:   a1 04 00 00 00             mov    eax,ds:0x4
      f0:   83 e8 01                        sub    eax,0x1
      f3:   a3 04 00 00 00              mov    ds:0x4,eax
    

3 个答案:

答案 0 :(得分:1)

您似乎正在使用pthreads,这是POSIX标准的一部分。 POSIX不允许你像这样“滚动你自己的”同步原语 - 它在4.11 Memory Synchronization中有这样说:

  

应用程序应确保更多地访问任何内存位置   比一个控制线程(线程或进程)受到限制   没有控制线程可以读取或修改内存位置   另一个控制线程可能正在修改它。这种访问是   限制使用同步线程执行的函数   与其他线程同步内存。

这样做的实际结果是允许编译器进行转换并执行可能会破坏您的假设的重新排序。

例如,编译器我已将while()循环优化为无限循环,因为它可以看到turn在设置它并在循环中测试它之间不能合法地修改,因为没有调用POSIX同步函数。这显然不会发生在你身上,但其他类似问题也是可能的。

在这种情况下,可能不是编译器优化会咬你,而是不尊重CPU内存模型。 This article描述了Peterson锁如何在多处理器x86上要求内存栅栏正确。 (POSIX同步原语的实现将包括必要的内存栅栏,但您的代码不包括。)

就代码而言,interest[1]的加载可以在写入interest[0]之前由x86重新排序,interest[0]的加载同样可以在写入之前重新排序interest[1]。这意味着两个线程可以同时看到interest[0] == 0interest[1] == 0,并且都进入关键部分。

答案 1 :(得分:0)

我不确定POSIX,但至少在标准C11中,你需要使用atomic_int来转向和兴趣。不是volatile

另外我可能会弄错(只知道c ++的规则)但是print_val函数中的非同步访问可能会导致未定义的行为,如果您在打印operation complete之前使用它。

答案 2 :(得分:0)

在读取/写入变量interest(内存位置interest[0]interest[1]),turn和{{1>时,您有竞争条件 }}。即增量和减量线程都对这些变量进行非同步访问。 <{1}}也可以访问*a,而其他线程可能正在更新它。

您需要一个同步机制,例如对这些变量进行互斥或原子访问,以使您的程序正常工作。