Нужен ли мьютекс для синхронизации простого флага между pthreads?

Давайте представим, что у меня есть несколько рабочих потоков, а именно:

while (1) {
    do_something();

    if (flag_isset())
        do_something_else();
}

У нас есть пара вспомогательных функций для проверки и установки флага:

void flag_set()   { global_flag = 1; }
void flag_clear() { global_flag = 0; }
int  flag_isset() { return global_flag; }

Таким образом, потоки продолжают вызывать do_something() в режиме busy-loop и в случае, если какой-либо другой поток установит global_flag, он также вызовет do_something_else() (что может, например, выводить прогресс или отладочную информацию при запросе, установив флаг из другого потока).

Мой вопрос: Нужно ли мне делать что-то специальное для синхронизации доступа к global_flag? Если да, то какова именно минимальная работа по выполнению синхронизации портативным способом?

Я пытался разобраться в этом, читая многие статьи, но до сих пор не совсем уверен в правильном ответе... Я думаю, что это один из следующих вариантов:

A: Нет необходимости в синхронизации, потому что установка или снятие флага не создает условий гонки:

Нам просто нужно определить флаг как volatile, чтобы убедиться, что он действительно считывается из общей памяти каждый раз, когда он проверяется:

volatile int global_flag;

Он может не распространяться сразу на другие ядра процессора, но рано или поздно это будет гарантировано.

B: Полная синхронизация необходима, чтобы убедиться, что изменения флага распространяются между потоками:

Установка общего флага в одном ядре процессора не обязательно приведет к тому, что он будет виден другому ядру. Нам нужно использовать мьютекс, чтобы убедиться, что изменения флага всегда передаются путем аннулирования соответствующих строк кэша на других процессорах. Код становится следующим:

volatile int    global_flag;
pthread_mutex_t flag_mutex;

void flag_set()   { pthread_mutex_lock(flag_mutex); global_flag = 1; pthread_mutex_unlock(flag_mutex); }
void flag_clear() { pthread_mutex_lock(flag_mutex); global_flag = 0; pthread_mutex_unlock(flag_mutex); }

int  flag_isset()
{
    int rc;
    pthread_mutex_lock(flag_mutex);
    rc = global_flag;
    pthread_mutex_unlock(flag_mutex);
    return rc;
}

C: Синхронизация необходима для того, чтобы изменения флага распространялись между потоками:

Это то же самое, что и B, но вместо того, чтобы использовать мьютекс с обеих сторон (reader & writeer), мы устанавливаем его только в стороне записи. Так как логика не требует синхронизации, нам просто нужно синхронизировать (аннулировать другие кэши) при смене флага:

volatile int    global_flag;
pthread_mutex_t flag_mutex;

void flag_set()   { pthread_mutex_lock(flag_mutex); global_flag = 1; pthread_mutex_unlock(flag_mutex); }
void flag_clear() { pthread_mutex_lock(flag_mutex); global_flag = 0; pthread_mutex_unlock(flag_mutex); }

int  flag_isset() { return global_flag; }

Это позволило бы избежать непрерывной блокировки и разблокировки мьютекса, когда мы знаем, что флаг редко меняется. Мы просто используем побочный эффект мьютексов Pthreads, чтобы убедиться, что изменение распространяется.

Итак, какой из них?

Я думаю, что A и B - это очевидный выбор, B - более безопасно. Но как насчет C?

Если C в порядке, есть ли какой-нибудь другой способ заставить смену флага быть видимой на всех процессорах?

Есть один связанный с этим вопрос: Гарантирует ли защита переменной с помощью pthread mutex, что она также не кэшируется? ...но на самом деле это не дает ответа.

16
задан Community 23 May 2017 в 12:26
поделиться