这种互斥锁的实现是否会导致未定义的行为?

时间:2019-05-08 02:13:59

标签: c thread-safety locking pthreads

我需要控制sleep处理数据的频率。在该示例中,它只是增加了变量的值。我不能在main内使用main,因为我需要保持频率恒定(而且我不知道处理所有数据需要多长时间)。我只知道一个事实,我需要做的任何处理都少于2秒,因此我只需要防止x每两秒钟增加一次main

我找到的解决方案涉及使用两个互斥锁:将一个互斥锁在extra中并在extra线程中对其进行解锁,将另一个互斥锁在main中并在{{ 1}}。这个extra线程每个周期休眠2秒。

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

void  *extra(void *arg)
{
    pthread_mutex_t *lock = (pthread_mutex_t *) arg;
    while(1) {
        pthread_mutex_unlock(&lock[0]);
        pthread_mutex_lock(&lock[1]);
        sleep(2);
    }
}

int main()
{
    int x = 0;

    pthread_mutex_t lock[2];
    pthread_mutex_init(&lock[0], NULL);
    pthread_mutex_init(&lock[1], NULL);

    pthread_mutex_lock(&lock[1]);

    pthread_t extra_thread;
    pthread_create(&extra_thread, NULL, &extra, lock);

    while(1) {
        x += 1;
        printf("%d\n", x);

        pthread_mutex_lock(&lock[0]);
        pthread_mutex_unlock(&lock[1]);
    }
}

问题

之所以可行,是因为main不能两次锁定lock[0];它必须等到extra对其进行解锁。但是,根据The Open Group

  

尝试重新锁定互斥锁会导致死锁。如果有线程尝试   解锁尚未锁定的互斥锁或已解锁的互斥锁,   未定义的行为结果。

问题

基于此,我在这里看到两个问题:

  1. 如果main试图锁定lock[0]两次,则应该死锁。
  2. extra锁定的
  3. lock[0]解锁main应该是未定义的行为。

我的分析正确吗?

2 个答案:

答案 0 :(得分:5)

回答您的问题,

  
      
  1. 如果main试图锁定lock[0]两次,则应该死锁。
  2.   

是的,会的。除非您使用递归互斥锁,否则您的子线程将永远无法锁定互斥锁,因为main总是会锁定它。

  
      
  1. extra解锁lock[0](由main锁定)应该是未定义的行为。
  2.   

对于NORMAL 非健壮互斥体,这是POSIX documentation for pthread_mutex_unlock()的不确定行为。但是,DEFAULT互斥锁不必一定是NORMAL且不健壮,因此需要注意以下几点:

  

如果互斥锁类型为PTHREAD_MUTEX_DEFAULT,则pthread_mutex_lock() [和pthread_mutex_unlock()]的行为可能与上表中所述的其他三种标准互斥锁类型之一相对应。如果它不符合这三个条件之一,则对于标记的情况,行为是不确定的。

(请注意,我添加了pthread_mutex_unlock()。互斥锁行为表清楚地表明,非所有者的解锁行为在不同类型的互斥锁之间有所不同,甚至在“未锁定时解锁”中使用相同的“匕首”标记所有者”列中使用的“重新锁定”列,“匕首”标记是指我引用的脚注。)

健壮的NORMALERRORCHECKRECURSIVE互斥锁将在非所有者线程试图对其进行解锁且互斥锁保持锁定的情况下返回错误。

一个更简单的解决方案是使用一对信号量(以下代码故意缺少错误检查以及空行,否则将增加可读性,从而消除/减少任何垂直滚动条):

#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
sem_t main_sem;
sem_t child_sem;
void *child( void *arg )
{
    for ( ;; )
    {
        sem_wait( &child_sem );
        sleep( 2 );
        sem_post( &main_sem );
    }
    return( NULL );
}
int main( int argc, char **argv )
{
    pthread_t child_tid;
    sem_init( &main_sem, 0, 0 );
    sem_init( &child_sem, 0, 0 );
    pthread_create( &child_tid, NULL, child, NULL );
    int x = 0;
    for ( ;; )
    {
        // tell the child thread to go
        sem_post( &child_sem );
        // wait for the child thread to finish one iteration
        sem_wait( &main_sem );
        x++;
        printf("%d\n", x);
    }
    pthread_join( child_tid, NULL );
}

答案 1 :(得分:2)

理智的线程安全解决方案是一个条件变量:

//main thread
while(1) {
    x += 1;
    printf("%d\n", x);

    pthread_mutex_lock(&lock);
    pthread_cond_wait(&cond, &lock);
    pthread_mutex_unlock(&lock);
}

然后在卧铺线程中执行以下操作:

//sleeper thread
while(1) {
    pthread_cond_signal(&cond);
    sleep(2);
}

不过,您还可以使用高分辨率的睡眠和时间,从操作系统和睡眠中读取当前时间以及剩余时间直到下一个纪元。

下一个选项是使用timerfd以固定的间隔唤醒您。它可以让您知道是否错过了唤醒。