【问题标题】:Binary Semaphore program has extremely unpredictable output二进制信号量程序具有极其不可预测的输出
【发布时间】:2022-12-22 05:27:02
【问题描述】:

我的程序使用枚举作为信号量。有两种可能的值/状态(因为它是二进制信号量)。该程序编译正常。 signal() 和 wait() 看起来合乎逻辑。为什么程序行为如此不可预测?即使是整数 printf 也是错误的。这是代码:

#include <stdio.h>
#include <pthread.h>
typedef enum {IN_USE,NOT_IN_USE} binary_semaphore;
binary_semaphore s=NOT_IN_USE;
struct parameters{
    int thread_num;
};
void wait(){
    while(s==IN_USE);
    s=IN_USE;
}
void signal(){
    s=NOT_IN_USE;
}
void resource(void *params){
    //assuming parameter is a parameters struct.
    struct parameters *p=(struct parameters*)params;
    wait();
    printf("Resource is being used by thread %d\n",(*p).thread_num);
    signal();
}
int main(void){
    pthread_t threads[4];
    struct parameters ps[4]={{1},{2},{3},{4}};
    register int counter=0;
    while(counter++<4){
        pthread_create(&threads[counter],NULL,resource,(void*)&ps[counter]);
    }
    return 0;
}

我的代码有什么问题? 一些输出(是的,每次都不同):-

(NOTHING)
Resource is being used by thread 32514
Resource is being used by thread 0
Resource is being used by thread 0
Resource is being used by thread 32602
Resource is being used by thread -24547608

是不是垃圾值问题?

【问题讨论】:

  • 需要访问您的binary_semaphore原子并使用其他同步原语来锁定对它的访问。现在您将遇到数据争用和未定义的行为。
  • 作为一个典型的例子:如果两个线程等待信号量会怎样。第三个线程向它发出信号,第一个等待的线程退出循环。但是在它可以将其标记为正在使用之前,它被第二个等待线程抢占了。第二个等待线程退出循环并将其标记为正在使用。当第一个等待线程再次启动时,它将将其标记为正在使用。现在你有认为可以继续的线程,当它不是。您拥有的线程越多,这个问题就越严重。
  • 简而言之:不要创建自己的线程原语,使用已经存在的。
  • 另一方面,register 说明符如今确实没有实际意义,除了您不能使用指向 &amp; 运算符的指针获取此类变量的地址。编译器比我们任何人都更能决定什么应该进入寄存器或不进入寄存器。此外,通过使用普通的for循环,该循环会更容易阅读和理解:for (unsigned counter = 0; counter &lt; 4; ++counter) { ... }
  • 即时的问题是你的“聪明”while循环.别耍小聪明了,用for。它存在的理由很充分。不信,就在循环里面打印i的值。

标签: c enums pthreads semaphore garbage


【解决方案1】:

我的代码有什么问题?

很多事情,已经在 cmets 中进行了广泛讨论。最重要的一个是您的代码充斥着数据竞争。这是信号量经常用来防止的事情之一,但在这种情况下,是你的信号量本身是活泼的。未定义的行为结果。

其他问题包括

  • pthreads 线程函数必须返回void *,但你的返回void
  • 你超出了你的main()psthreads数组的边界
  • 程序退出前您没有加入您的线程。
  • 您定义的函数与 C 标准库函数 (signal()) 和标准 POSIX 函数 (wait()) 同名。

尽管如此,如果您的 C 实现支持原子选项,那么您可以使用它来实现一个与原始代码没有太大区别的工作信号量:

#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>

// This is used only for defining the enum constants
enum sem_val { NOT_IN_USE, IN_USE };

// It is vital that sem be *atomic*.
// With `stdatomic.h` included, "_Atomic int" could also be spelled "atomic_int".
_Atomic int sem = ATOMIC_VAR_INIT(NOT_IN_USE);

struct parameters{
    int thread_num;
};

void my_sem_wait() {
    int expected_state = NOT_IN_USE;

    // See discussion below
    while (!atomic_compare_exchange_strong(&sem, &expected_state, IN_USE)) {
        // Reset expected_state
        expected_state = NOT_IN_USE;
    }
}

void my_sem_signal() {
    // This assignment is performed atomically because sem has atomic type
    sem = NOT_IN_USE;
}

void *resource(void *params) {
    //assuming parameter is a parameters struct.
    struct parameters *p = params;

    my_sem_wait();
    printf("Resource is being used by thread %d
", p->thread_num);
    my_sem_signal();

    return NULL;
}

int main(void) {
    pthread_t threads[4];
    struct parameters ps[4] = {{1},{2},{3},{4}};

    for (int counter = 0; counter < 4; counter++) {
        pthread_create(&threads[counter], NULL, resource, &ps[counter]);
    }
    // It is important to join the threads if you care that they run to completion
    for (int counter = 0; counter < 4; counter++) {
        pthread_join(threads[counter], NULL);
    }
    return 0;
}

其中大部分内容非常简单,但 my_sem_wait() 函数有更多解释。它使用原子比较和交换来确保线程只有在将信号量的值从NOT_IN_USE更改为IN_USE时才会继续,比较和条件赋值作为单个原子单元执行。具体来说,这...

    atomic_compare_exchange_strong(&sem, &expected_state, IN_USE)

...说“原子地,将sem的值与expected_state的值进行比较,如果它们比较相等,则将值IN_USE分配给sem。”该函数另外将 expected_state 的值设置为从 sem 读取的值(如果它们不同),并返回所执行的相等比较的结果(等效于:如果指定值被分配给 sem 则返回 1 和 0如果不)。

必须将比较和交换作为一个原子单元来执行。单独的原子读取和写入将确保没有数据竞争,但它们不能确保正确的程序行为,因为等待信号量的两个线程都可以同时看到它可用,每个线程都有机会标记它不可用。然后两者都会继续。原子比较和交换可防止一个线程在另一个线程读取和更新该值之间读取信号量的值。

但是请注意,与 pthreads 互斥量或 POSIX 信号量不同,此信号量等待忙着.这意味着等待获取信号量的线程在获取信号量时会消耗 CPU,而不是像等待 pthreads 互斥量的线程那样进入休眠状态。如果信号量访问通常是无竞争的,或者如果线程永远不会将其锁定很长时间,那么这可能没问题,但在其他情况下,它会使您的程序比需要的更需要资源。

【讨论】:

    【解决方案2】:

    您遇到了竞争条件以及未定义的行为。当您在多线程应用程序中使用常规 int 作为信号量时,无法保证一个进程会读取变量的值并在另一个进程能够读取它之前修改它,这就是为什么您必须使用为您设计的并发库操作系统。此外,您传递给pthread_create 的函数指针不是正确的类型,这是未定义的行为。

    我将你的“信号量”enum 替换为指向pthread_mutex_t 的指针,该指针在main() 的堆栈上初始化,并且每个线程都将指向它的指针作为其struct parameter 的成员。

    我还将 void resource(void* params) 的定义更改为 void* resource(void *params),因为这是一个匹配 pthread_create 期望的第三个参数的原型。

    您的 wait()signal() 函数可以一对一替换为 pthread_mutex_lock()pthread_mutex_unlock() 来自您已经包含的 pthread.h

    #include <stdio.h>
    #include <pthread.h>
    
    struct parameters{
        int thread_num;
        pthread_mutex_t *mutex; //mutex could be global if you prefer
    };
    
    void* resource(void *params){ //pthread_create expects a pointer to a function that takes a void* and returns a void*
        //assuming parameter is a parameters struct.
        struct parameters *p = (struct parameters*)params;
        pthread_mutex_lock(p->mutex); 
        printf("Resource is being used by thread %d
    ", p->thread_num);
        pthread_mutex_unlock(p->mutex);
        return NULL;
    }
    
    int main(void){
        pthread_t threads[4];
        pthread_mutex_t mutex;
        pthread_mutex_init(&mutex, NULL);
        struct parameters ps[4]={{1, &mutex},{2, &mutex},{3, &mutex},{4, &mutex}};
        for(int counter = 0; counter < 4; ++counter)
            pthread_create(&threads[counter], NULL, resource, &ps[counter]);
        //Threads should be joined
        for(int counter = 0; counter < 4; ++counter)
            pthread_join(threads[counter], NULL);
    }
    

    这将消除您正在经历的随机性。

    【讨论】:

      猜你喜欢
      • 2021-08-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-12-23
      • 2015-07-12
      • 2013-07-15
      • 1970-01-01
      • 2023-03-29
      相关资源
      最近更新 更多