二进制信号量保持并发

Binary semaphore to maintain concurrency

我正在尝试使用二进制信号量实现多线程程序。这是代码:

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

int g = 0;

sem_t *semaphore;

void *myThreadFun(void *vargp)
{
    int myid = (int)vargp;
    static int s = 0;
    sem_wait(semaphore);
    ++s; ++g;
    printf("Thread ID: %d, Static: %d, Global: %d\n", myid, s, g);
    fflush(stdout);
    sem_post(semaphore);
    pthread_exit(0);
}

int main()
{   

    int i;
    pthread_t tid;
    if ((semaphore = sem_open("/semaphore", O_CREAT, 0644, 3))==SEM_FAILED) {
    printf("semaphore initialization failed\n");
    }


    for (i = 0; i < 3; i++) {
        pthread_create(&tid, NULL, myThreadFun, (void *)i);
    }

    pthread_exit(NULL);
    return 0; 
}

现在,当我打开 sempahore 时,我将计数设为 3。我原以为这不会起作用,而且我会出现竞争条件,因为每个线程现在都可以递减计数。

执行有问题吗?另外,如果我在 sem_open 期间使计数为 0,那不会启动死锁条件,因为所有线程都应该在 sem_wait.

上被阻塞

稍后我将介绍证明您存在竞争条件的代码。我将添加几种不同的方式来触发它,以便您了解它是如何工作的。我在 Linux 上执行此操作并将 -std=gnu99 作为参数传递给 gcc ie

gcc -Wall -pedantic -lpthread -std=gnu99 semaphore.c -o semtex

注意。在您的原始示例中(假设 Linux),您犯的一个致命错误是没有删除信号量。如果您 运行 以下命令,您可能会在您的机器上看到其中一些

ls -la /dev/shm/sem.*

在 运行 启动程序之前,您需要确保文件系统中没有旧信号量,否则您最终会从旧信号量中获取最后的设置。您需要使用 sem_unlink 来清理。

要运行请使用以下内容。

rm /dev/shm/sem.semtex;./semtex

我特意确保信号量在 运行ning 之前不存在,因为如果你有一个 DEADLOCK,它可能会留下来,这会在测试时导致各种问题。

Now, when I opened the sempahore, I made the count 3. I was expecting that this wouldn't work, and I would get race condition, because each thread is now capable of decrementing the count.

你有一个竞争条件,但有时 C 太快了似乎 可以工作,因为你的程序可以在 OS 的时间内完成它需要的一切已分配给线程,即 OS 没有在重要时刻抢占它。

这就是其中一种情况,竞争条件就在那里,你只需要眯着眼睛就能看到它。在下面的代码中,您可以调整一些参数以查看死锁、正确用法和未定义行为。

#define INITIAL_SEMAPHORE_VALUE CORRECT

INITIAL_SEMAPHORE_VALUE可以取三个值...

#define DEADLOCK  0
#define CORRECT   1
#define INCORRECT 2

我希望它们是不言自明的。你也可以使用两种方法来导致竞争条件炸毁程序。

#define METHOD sleep

METHOD 设置为 spin 然后你可以使用 SPIN_COUNT 并在你真正看到问题之前找出循环可以 运行 多少次,这是 C,它可以在被抢占之前完成很多工作。该代码包含您需要的大部分信息。

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h> 
#include <unistd.h>
#define DEADLOCK  0 // DEADLOCK
#define CORRECT   1 // CORRECT
#define INCORRECT 2 // INCORRECT
/*
 * Change the following values to observe what happen.
 */
#define INITIAL_SEMAPHORE_VALUE CORRECT
//The next value provides to two different ways to trigger the problem, one
//using a tight loop and the other using a system call.
#define METHOD sleep

#if (METHOD == spin)
/* You need to increase the SPIN_COUNT to a value that's big enough that the
 * kernel preempts the thread to see it fail. The value set here worked for me 
 * in a VM but might not work for you, tweak it. */
#define SPIN_COUNT 1000000
#else 
/* The reason we can use such a small time for USLEEP is because we're making
 * the kernel preempt the thread by using a system call.*/
#define USLEEP_TIME 1
#endif
#define TOT_THREADS 10

static int g = 0;
static int ret = 1729;
sem_t *semaphore;

void *myThreadFun(void *vargp) {
  int myid = (int)vargp;
  int w = 0;
  static int s = 0;
  if((w = sem_wait(semaphore)) != 0) {
    fprintf(stderr, "Error: %s\n", strerror(errno));
    abort();
  };
/* This is the interesting part... Between updating `s` and `g` we add
 * a delay using one of two methods. */
  s++;
#if ( METHOD == spin )
  int spin = 0;
  while(spin < SPIN_COUNT) {
    spin++;
  }
#else
  usleep(USLEEP_TIME);
#endif
g++;
  if(s != g) {
    fprintf(stderr, "Fatal Error: s != g in thread: %d, s: %d, g: %d\n", myid, s, g);
    abort();
  } 
  printf("Thread ID: %d, Static: %d, Global: %d\n", myid, s, g);
  // It's a false sense of security if you think the assert will fail on a race
  // condition when you get the params to sem_open wrong It might not be
  // detected.
  assert(s == g);
  if((w = sem_post(semaphore)) != 0) {
    fprintf(stderr, "Error: %s\n", strerror(errno));
    abort();
  };
  return &ret;
} 

int main(void){
  int i;
  void *status;
  const char *semaphore_name = "semtex";
  pthread_t tids[TOT_THREADS];
  if((semaphore = sem_open(semaphore_name, O_CREAT, 0644, INITIAL_SEMAPHORE_VALUE)) == SEM_FAILED) {
    fprintf(stderr, "Fatal Error: %s\n", strerror(errno));
    abort();
  }
  for (i = 0; i < TOT_THREADS; i++) {
    pthread_create(&tids[i], NULL, myThreadFun, (void *) (intptr_t) i);
  }
  for (i = 0; i < TOT_THREADS; i++) {
    pthread_join(tids[i], &status);
    assert(*(int*)status == 1729);
  }
  /*The following line was missing from your original code*/
  sem_unlink(semaphore_name);
  pthread_exit(0);
}

Now, when I opened the sempahore, I made the count 3. I was expecting that this wouldnt work, and I would get race condition, because each thread is now capable of decrementing the count.

那你怎么判断没有种族呢?在没有数据竞争的情况下观察到的输出与您可以依赖的输出一致,绝不能证明不存在数据竞争。它只是未能提供任何证据。

但是,您似乎暗示在多个线程中会发生数据竞争 inherent,同时对一个信号量执行 sem_wait(),其值最初是大于 1(否则你说的是哪个计数器?)。但那完全是胡说八道。你说的是 信号量 。它是一个同步对象。这些对象和操作它们的函数是线程同步的基础。它们本身要么是完全线程安全的,要么是最终错误的。

现在,你是正确的,你打开信号量的初始计数足以避免任何线程在 sem_wait() 中阻塞,因此它们可以在整个主体中同时 运行 myThreadFun() 个。但是,您尚未确定它们实际上 运行 同时进行。他们可能不这样做的原因有几个。如果他们同时执行 运行,那么共享变量 sg 的递增确实值得关注,但同样,即使您没有看到数据竞争的迹象,也不会表示没有。

除此之外,您的线程都调用 sem_wait()sem_post()printf() 这一事实会以内存屏障的形式引起一些同步,这会降低观察对 sg 的异常影响。 sem_wait()sem_post() 必须包含内存屏障才能正常运行,无论信号量的当前计数如何。 printf() 调用需要使用锁定来保护流的状态免受多线程程序中的损坏,并且可以合理地假设这将需要内存屏障。

Is there something wrong with the implementation?

是的。它没有正确同步。用计数 1 初始化信号量,以便 sg 的修改仅在恰好一个线程锁定信号量时发生。

Also, if I make the count 0 during sem_open, wouldnt that initiate a deadlock condition, because all the threads should be blocked on sem_wait.

如果在启动任何其他线程之前信号量的计数为 0,则可以。因此,打开计数为 0 的信号量是不合适的,除非您随后在启动线程之前也对它 post。但是您正在使用 named 信号量。这些一直存在直到被移除,而你永远不会移除它。除非需要创建新的信号量,否则您指定给 sem_open() 的计数无效;打开现有信号量时,其计数不变。

此外,在主线程终止之前,一定要让主线程加入所有其他线程。不这样做本身并不是错误的,但在大多数情况下,它是您想要的语义所必需的。