Дилемма атомики: Может ли он даже заменить мьютексы в любом удобном случае?

#gcc #atomic #c11

Вопрос:

Я использую C11 atomics в встроенной системе компиляции GNU (в основном такой же, как std::atomics). У меня возникают проблемы с поиском вариантов их использования, даже в очень простом примере, над которым я работаю. Сравните следующий очень простой дизайн программы: я хочу, чтобы ThreadA всегда записывала, а ThreadB считывал информацию, хранящуюся в общей строке C.

Первая попытка:

 #include <string.h>
#include <cstdlib>
#define CONFIGURED_MAX 128

static char* shared_ptr_to_str;

char* get_that_char()
{
    char* str = NULL;
    __atomic_load(shared_ptr_to_str, str, __ATOMIC_ACQUIRE);
    return str;
}

void threadA()
{
    char some[] = "Some String we got over network somehow";
    char * tmp_ptr = (char*) malloc(CONFIGURED_MAX);
    strncpy(tmp_ptr, some, CONFIGURED_MAX);
    __atomic_store(shared_ptr_to_str, tmp_ptr, __ATOMIC_RELEASE);
}

void threadB()
{
    char* grabbed_str = get_that_char();

    // use grabbed_str somehow
}
 

У этого подхода уже есть различные проблемы:

  1. Очевидно, что это не освобождает память, ранее использовавшуюся для хранения строки, когда выполняется итерация потока.
  2. Я держусь за строку в ThreadB (), которая может измениться в любой момент.

Когда я пытаюсь решить эти проблемы, мы приходим к ситуации B

Вторая попытка:

 #include <string.h>
#include <cstdlib>
#define CONFIGURED_MAX 128

static char* shared_ptr_to_str;

void cpy_that_char(char** to_fill)
{
    char *str = NULL;
    __atomic_load(shared_ptr_to_str, str, __ATOMIC_ACQUIRE);
    *to_fill = (char*) malloc(strnlen(str, CONFIGURED_MAX));
    strncpy(*to_fill, str, CONFIGURED_MAX);
}

void threadA()
{
    char some[] = "Some String we got over network somehow";
    // free old buffer first
    free(shared_ptr_to_str);

    // fill in new stuff
    char * tmp_ptr = (char*) malloc(strnlen(some, CONFIGURED_MAX));
    strncpy(tmp_ptr, some, strnlen(some, CONFIGURED_MAX));
    __atomic_store(shared_ptr_to_str, tmp_ptr, __ATOMIC_RELEASE);
}

void threadB()
{
    char* grabbed_str = NULL;
    cpy_that_char(amp;grabbed_str);

    // use grabbed_str somehow
}
 

Теперь я сделал это еще хуже! Хотя теперь я получаю локальную копию строки для ThreadB (чтобы он мог делать с ней все, что ему заблагорассудится), атомарные операции все равно могут мешать друг другу:

  1. В потоке между освобождением и переназначением памяти cpy_that_char() может быть вызван из ThreadB и столкнуться с освобожденной памятью.
  2. В функции cpy_that char функция strnlen() уже больше не гарантированно столкнется с тем же shared_ptr_to_str, когда она вызывается после атомной загрузки. Адрес памяти мог быть освобожден между ними с помощью ThreadA()

Это означает, что я должен сгруппировать вызовы вместе: free() должен быть сгруппирован с store в ThreadA, а load должен быть сгруппирован с strnlen() в cpy_that_char() из ThreadB.

И это в основном означает, что мы вернулись к мьютексам…

Почти в любом случае я сталкивался с подобной ситуацией. На первый взгляд кажется, что это можно решить с помощью атомики, но я снова и снова возвращаюсь к мьютексам. Может ли кто-нибудь сказать мне, что такое реальный случай использования атомики и мог ли я решить приведенный выше пример с помощью атомики?

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

1. Посмотрите «алгоритмы без блокировки». Есть некоторые вещи, которые можно сделать с помощью атомики вместо мьютекса, хотя это может быть довольно сложно исправить.

Ответ №1:

Атомики полезны для примитивов. Например, если поток A обрабатывает элементы, а поток B сообщает о ходе выполнения, то поток A может записать количество обработанных элементов в атомарное целое число, а поток B может прочитать его без необходимости мьютекса.

Они также полезны, если есть несколько авторов с одинаковым значением. В приведенном выше примере счетчик выполнения может быть атомарно увеличен из нескольких потоков обработки.

Другим вариантом использования является флаг «выход», записываемый из основного потока и регулярно считываемый рабочими потоками, чтобы проверить, следует ли им завершать работу.

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

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

1. Я тоже это подозревал, но подумал, что, может быть, я просто что-то упускаю. Спасибо за ваш прагматичный ответ!