атомарный обмен с CAS (используя встроенные функции gcc sync) - PullRequest
5 голосов
/ 04 июня 2010

Можно ли использовать функцию сравнения и обмена для атомного обмена переменных? Я использую C / C ++ через gcc на x86_64 RedHat Linux, в частности, встроенные функции __sync. Пример:

   int x = 0, y = 1; 
   y = __sync_val_compare_and_swap(&x, x, y);

Я думаю, что это сводится к тому, может ли x меняться между & x и x; например, если & x представляет собой операцию, возможно, что x изменится между & x и x в аргументах. Я хочу предположить, что приведенное выше сравнение всегда будет верным; мой вопрос, могу ли я. Очевидно, есть булевая версия CAS, но тогда я не могу заставить старый x записать в y.

Более полезным примером может быть вставка или удаление из заголовка связанного списка (gcc утверждает, что поддерживает типы указателей, поэтому предположим, что это элементы elem и head):

   elem->next = __sync_val_compare_and_swap(&head, head, elem); //always inserts?
   elem = __sync_val_compare_and_swap(&head, head, elem->next); //always removes?

Ссылка: http://gcc.gnu.org/onlinedocs/gcc/Atomic-Builtins.html

Ответы [ 3 ]

3 голосов
/ 04 июня 2010

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

Итак, ваш пример:

elem->next = __sync_val_compare_and_swap(&head, head, elem); //always inserts?

не обязательно будет вставлять новый элемент. Если другой поток вставляет элемент в тот же момент, существует условие гонки, которое может привести к тому, что вызов этого потока к __sync_val_compare_and_swap() не обновит head (но ни элемент этого потока, ни элемент другого потока еще не потерян, если вы обрабатываете его правильно) ,

Но есть еще одна проблема с этой строкой кода - даже если head действительно обновился, есть короткий момент времени, когда head указывает на вставленный элемент, но указатель next этого элемента не был обновлен, чтобы указывать на предыдущий заголовок списка. Если другой поток подключается в этот момент и пытается просмотреть список, происходят плохие вещи.

Чтобы правильно обновить список, измените эту строку кода на что-то вроде:

whatever_t* prev_head = NULL;
do {
    elem->next = head;  // set up `elem->head` so the list will still be linked 
                        // correctly the instant the element is inserted
    prev_head = __sync_val_compare_and_swap(&head, elem->next, elem);
} while (prev_head != elem->next);

Или используйте вариант bool, который мне кажется немного более удобным:

do {
    elem->next = head;  // set up `elem->head` so the list will still be linked 
                        // correctly the instant the element is inserted
} while (!__sync_bool_compare_and_swap(&head, elem->next, elem));

Это немного уродливо, и я надеюсь, что я понял это правильно (легко разобраться в деталях поточно-безопасного кода). Он должен быть заключен в функцию insert_element() (или, что еще лучше, использовать соответствующую библиотеку).

Решение проблемы ABA:

Я не думаю, что проблема ABA имеет отношение к этому коду "добавить элемент в начало списка". Допустим, поток хочет добавить объект X в список, и когда он выполняет elem->next = head, head имеет значение A1.

Затем перед выполнением __sync_val_compare_and_swap() появляется другой набор потоков:

  • удаляет A1 из списка, head указывает на B
  • делает что-либо с объектом A1 и освобождает его
  • выделяет другой объект, A2, который находится по тому же адресу, что и A1 был
  • добавляет A2 в список, так что head теперь указывает на A2

Поскольку A1 и A2 имеют одинаковый идентификатор / адрес, это является примером проблемы ABA.

Однако в данном случае это не имеет значения, поскольку поток, добавляющий объект X, не заботится о том, чтобы head указывал на объект, отличный от того, с которого он начинал - все, о чем он заботится, это когда X в очереди:

  • список согласован,
  • объекты в списке не были потеряны, а
  • никаких объектов, кроме X, не было добавлено в список (по этой теме)
0 голосов
/ 05 октября 2013

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

Однако у вас все еще есть проблемы с условиями гонки между назначениями на y. Иногда y является локальным, и в этом случае это будет безопасно, но если совместно используются x и y, у вас есть серьезная проблема, и для ее решения потребуется блокировка.

0 голосов
/ 04 июня 2010

Неа. Инструкция CAS на x86 берет значение из регистра и сравнивает / записывает его со значением в памяти.

Чтобы атомарно поменять местами две переменные, нужно было бы работать с двумя операндами памяти.

А может ли x меняться от &x до x? Да, конечно, может.

Даже без & оно может измениться.

Даже в такой функции, как Foo(x, x), вы можете получить два разных значения x, поскольку для вызова функции компилятор должен:

  • взять значение x и сохранить его в позиции первого параметра в соответствии с соглашением о вызовах
  • взять значение x и сохранить его в позиции второго параметра в соответствии с соглашением о вызовах

между этими двумя операциями другой поток может легко изменить значение x.

...