pthread_cond_signal() не дает достаточно времени для запуска сигнального потока

#c #pthreads #c99

#c #pthreads #c99

Вопрос:

Поток, вызывающий pthread_cond_signal, повторно захватывает мьютекс до того, как сигнальный поток может быть освобожден.

Приведенный ниже код показывает простой пример рассматриваемой проблемы. Основной поток удержит блокировку, создаст рабочий поток, а затем войдет в цикл, который печатает данные по мере их поступления. Сигнал о запуске подается через условную переменную.

Рабочий поток войдет в цикл, который генерирует данные, захватывает блокировку, записывает данные, а затем передает сигнал основному потоку.

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

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int data = 0;

void *thread(void *arg) {
    int length = *(int *) arg;
    for (int i = 0; i < length; i  ) {
        // Do some work
        pthread_mutex_lock(amp;lock);
        data = i;
        fprintf(stdout, "Writingn");
        pthread_cond_signal(amp;cond);
        pthread_mutex_unlock(amp;lock);
    }
    pthread_exit(0);
}


int main(int argc, const char *argv[]) {
    pthread_t worker;
    int length = 4;

    pthread_mutex_lock(amp;lock);
    pthread_create(amp;worker, 0, thread, amp;length);

    for (int i = 0; i < length; i  ) {
        fprintf(stdout, "Waitingn");
        pthread_cond_wait(amp;cond, amp;lock);
        fprintf(stdout, "read data: %dn", data);
    }
    pthread_mutex_unlock(amp;lock);

    pthread_join(worker, NULL);
    return 0;
}
  

Это даст следующий результат:

 Waiting
Writing
Writing
Writing
Writing
read data: 3
Waiting
  

Ожидаемое поведение:
Основной поток удерживает мьютекс и освобождает его только после ожидания. Затем рабочий поток запишет свои данные и передаст сигнал основному потоку. Основной поток немедленно заблокирует мьютекс по сигналу, затем прочитает данные и вернется к ожиданию, отпуская мьютекс. Тем временем рабочий поток выполнит свою работу и будет ждать, пока основной поток снова не будет ждать, чтобы записать свои данные и передать их.

Вместо этого кажется, что рабочий поток получает мьютекс сразу после вызова signal , редко позволяя основному потоку получить доступ. Если я переведу рабочий поток в режим ожидания вместо // Do some work , это даст ожидаемый результат.

Комментарии:

1. Почему это неожиданно?

2. @immibis Поскольку основной поток ожидает cond, я ожидал, что он немедленно перестанет блокироваться, как только ему будет подан сигнал, и будет иметь приоритетное требование мьютекса. Вместо этого рабочий поток может получить доступ к мьютексу раньше, чем это сделает основной поток.

3. Вы должны использовать sched_yield() вместо того, чтобы спать.

4. @Cyrusc Очевидно, что для разблокировки основного потока требуется больше времени, чем для повторного получения мьютекса рабочим потоком. Когда вещи многопоточны, вы не можете выбирать, в каком порядке они происходят!

Ответ №1:

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

У вас никогда не должно быть переменной условия без фактического условия для некоторого общего состояния, которого вы ожидаете — возврат из a pthread_cond_wait() не означает, что поток должен обязательно продолжаться, это означает, что он должен проверить, является ли условие, которого он ожидал, истинным. Вот почему они называются условными переменными.

В этом случае состояние, которое ваш поток записи хочет дождаться, это «основной поток использовал последние данные, которые я записал».. Однако ваш поток чтения (основной) также должен ожидать выполнения условия — «поток записи записал некоторые новые данные». Вы можете выполнить оба этих условия с помощью переменной flag, которая указывает, что в переменную были записаны некоторые новые, неиспользованные данные data . Флаг не устанавливается, устанавливается потоком записи при обновлении data и не устанавливается основным потоком при чтении data . Поток записи ожидает снятия флага, а поток чтения ожидает установки флага.

При таком расположении вам также не нужно блокировать мьютекс при запуске потока записи — не имеет значения, в каком порядке запускаются потоки, потому что все согласовано в любом случае.

Обновленный код выглядит следующим образом:

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

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int data = 0;
int data_available = 0;

void *thread(void *arg)
{
    int length = *(int *) arg;
    for (int i = 0; i < length; i  ) {
        // Do some work
        pthread_mutex_lock(amp;lock);
        fprintf(stdout, "Waiting to writen");
        while (data_available)
            pthread_cond_wait(amp;cond, amp;lock);
        fprintf(stdout, "Writingn");
        data = i;
        data_available = 1;
        pthread_cond_signal(amp;cond);
        pthread_mutex_unlock(amp;lock);
    }
    pthread_exit(0);
}


int main(int argc, const char *argv[])
{
    pthread_t worker;
    int length = 4;

    pthread_create(amp;worker, 0, thread, amp;length);

    for (int i = 0; i < length; i  ) {
        pthread_mutex_lock(amp;lock);
        fprintf(stdout, "Waiting to readn");
        while (!data_available)
            pthread_cond_wait(amp;cond, amp;lock);
        fprintf(stdout, "read data: %dn", data);
        data_available = 0;
        pthread_cond_signal(amp;cond);
        pthread_mutex_unlock(amp;lock);
    }

    pthread_join(worker, NULL);
    return 0;
}
  

Конечно, потоки в конечном итоге работают синхронно, но по сути у вас есть производитель-потребитель с максимальной длиной очереди 1, так что это ожидаемо.