Приводит ли эта реализация блокировок мьютекса к неопределенному поведению? - PullRequest
7 голосов
/ 08 мая 2019

Мне нужно контролировать частоту, с которой main обрабатывает данные.В этом примере это просто увеличивает значение переменной.Я не могу использовать sleep внутри main, потому что мне нужно, чтобы частота была постоянной (и я не знаю точно, сколько времени занимает обработка всех данных).Просто я точно знаю, что любая необходимая обработка занимает менее 2 секунд, поэтому мне просто нужно предотвратить увеличение main x чаще, чем раз в две секунды.

Решение, которое я 'Мы обнаружили, что используется два мьютекса: блокировка одного в main и разблокировка его в потоке extra, блокировка другого в extra и разблокировка в main.Этот поток 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 не разблокирует его.Однако, согласно Открытая группа

Попытка разблокировать мьютекс приводит к тупику.Если поток пытается разблокировать мьютекс, который он не заблокировал, или мьютекс, который разблокирован, это приводит к неопределенному поведению.

Вопрос

Исходя из этого, я вижу здесь две проблемы:

  1. Если main пытается заблокировать lock[0] дважды, это должно привести к взаимоблокировке.
  2. extra разблокировка lock[0], заблокированная main, должна быть неопределеннойповедение.

Правильно ли проведен анализ?

Ответы [ 2 ]

5 голосов
/ 08 мая 2019

Отвечая на ваши вопросы,

  1. Если main дважды попытается заблокировать lock[0], он должен зайти в тупик.

Да, это будет,Если вы не используете рекурсивные мьютексы, но тогда ваш дочерний поток никогда не сможет заблокировать мьютекс, поскольку main всегда будет блокировать его.

extra разблокировка lock[0], которая была заблокирована главным, должна иметь неопределенное поведение.

Согласно документации POSIX для pthread_mutex_unlock(), этоявляется неопределенным поведением для ненадежного мьютекса NORMAL и .Однако мьютекс DEFAULT не обязательно должен быть NORMAL и не устойчивым, поэтому есть следующее предупреждение:

Если тип мьютекса равен PTHREAD_MUTEX_DEFAULT, поведение pthread_mutex_lock()pthread_mutex_unlock()] может соответствовать одному из трех других стандартных типов мьютекса, как описано в таблице выше.Если он не соответствует ни одному из этих трех, поведение не определено для отмеченных случаев.

(Обратите внимание на мое добавление pthread_mutex_unlock(). Таблица поведения мьютекса четко показывает, что поведение разблокировки дляне владелец различается в разных типах мьютексов и даже использует один и тот же знак «кинжал» в столбце «Разблокировать, когда не владелец», который используется в столбце «Переблокировать», а знак «кинжал» относится к цитируемой сноске.)

Надежный мьютекс NORMAL, ERRORCHECK или RECURSIVE вернет ошибку, если не владеющий поток попытается разблокировать его, и мьютекс останется заблокированным.

ПрощеРешение состоит в том, чтобы использовать пару семафоров (в следующем коде намеренно отсутствует проверка ошибок вместе с пустыми строками, которые в противном случае повысили бы читаемость для устранения / уменьшения любой вертикальной полосы прокрутки):

#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 );
}
2 голосов
/ 08 мая 2019

Нормальное поточно-ориентированное решение - это условная переменная:

//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, чтобы разбудить вас через фиксированный интервал. И он может дать вам знать, если вы пропустили пробуждение.

...