Подтвердить что ты не робот

Является std:: atomic_compare_exchange_weak потоком-небезопасным по дизайну?

На странице cppreference atomic_compare_exchange было показано, что существующие реализации std::atomic_compare_exchange_weak вычисляют логический результат CAS с неатомным сравнением инструкция, например

    lock
    cmpxchgq   %rcx, (%rsp)
    cmpq       %rdx, %rax

который (Edit: извинения за красную селедку)

Разбить CAS-контуры, такие как Concurrency в списке действий 7.2:

while(!head.compare_exchange_weak(new_node->next, new_node);

Спецификация (29.6.5 [atomics.types.operations.req]/21-22), по-видимому, подразумевает, что результат сравнения должен быть частью атомной операции:

Эффекты: атомарно сравниваются...

Возвращает: результат сравнения

но действительно ли это возможно? Должны ли мы сообщать об ошибках поставщикам или LWG?

4b9b3361

Ответ 1

TL; DR: atomic_compare_exchange_weak безопасен по дизайну, но фактические реализации ошибочны.

Здесь код, который действительно генерирует Clang для этого небольшого фрагмента:

struct node {
  int data;
  node* next;
};

std::atomic<node*> head;

void push(int data) {
  node* new_node = new node{data};
  new_node->next = head.load(std::memory_order_relaxed);
  while (!head.compare_exchange_weak(new_node->next, new_node,
      std::memory_order_release, std::memory_order_relaxed)) {}
}

Результат:

  movl  %edi, %ebx
  # Allocate memory
  movl  $16, %edi
  callq _Znwm
  movq  %rax, %rcx
  # Initialize with data and 0
  movl  %ebx, (%rcx)
  movq  $0, 8(%rcx) ; dead store, should have been optimized away
  # Overwrite next with head.load
  movq  head(%rip), %rdx
  movq  %rdx, 8(%rcx)
  .align  16, 0x90
.LBB0_1:                                # %while.cond
                                        # =>This Inner Loop Header: Depth=1
  # put value of head into comparand/result position
  movq  %rdx, %rax
  # atomic operation here, compares second argument to %rax, stores first argument
  # in second if same, and second in %rax otherwise
  lock
  cmpxchgq  %rcx, head(%rip)
  # unconditionally write old value back to next - wait, what?
  movq  %rax, 8(%rcx)
  # check if cmpxchg modified the result position
  cmpq  %rdx, %rax
  movq  %rax, %rdx
  jne .LBB0_1

Сравнение совершенно безопасно: оно просто сравнивает регистры. Однако вся операция небезопасна.

Критическая точка такова: описание compare_exchange_ (слабый | сильный) говорит:

Атомно [...], если true, замените содержимое точки памяти на это на нужное, а если false, обновит содержимое памяти в ожидании с содержимым памяти, на которое указывает этот

Или в псевдокоде:

if (*this == expected)
  *this = desired;
else
  expected = *this;

Обратите внимание, что expected записывается только в , если сравнение ложно, а *this записывается только в , если сравнение истинно. Абстрактная модель С++ не разрешает выполнение, в которое оба записываются. Это важно для правильности push выше, потому что, если происходит запись в head, вдруг new_node указывает на местоположение, которое видимо для других потоков, что означает, что другие потоки могут начать чтение next (путем доступа к head->next > ), и если также записывается запись в expected (которая псевдонизирует new_node->next), то раса.

И Clang пишет в new_node->next безоговорочно. В случае, когда сравнение верно, что изобретенная запись.

Это ошибка в Clang. Я не знаю, делает ли GCC то же самое.

Кроме того, формулировка стандарта субоптимальна. Он утверждает, что вся операция должна произойти атомарно, но это невозможно, потому что expected не является атомарным объектом; пишет, там не может произойти атомарно. Что должен сказать стандарт, так это то, что сравнение и запись в *this происходят атомарно, но запись на expected не выполняется. Но это не так уж плохо, потому что никто на самом деле не ожидает, что эта запись будет атомарной.

Таким образом, должен быть отчет об ошибке для Clang (и, возможно, GCC), и отчет о дефектах для стандарта.

Ответ 2

Я был тем, кто изначально нашел эту ошибку. Последние несколько дней я отправлял электронное письмо Энтони Уильямсу относительно этой проблемы и реализации поставщика. Я не понимал, что Кубби поднял вопрос StackOverFlow. Это не просто Clang или GCC - это каждый продающийся разработчик (все, что имеет значение в любом случае). Энтони Уильямс также автор Just:: Thread (поток С++ 11 и атомная библиотека) подтвердил, что его библиотека реализована правильно (только известная правильная реализация).

Энтони поднял отчет об ошибке GCC http://gcc.gnu.org/bugzilla/show_bug.cgi?id=60272

Простой пример:

   #include <atomic>
   struct Node { Node* next; };
   void Push(std::atomic<Node*> head, Node* node)
   {
       node->next = head.load();
       while(!head.compare_exchange_weak(node->next, node))
           ;
   }

g++ 4.8 [ассемблер]

       mov    rdx, rdi
       mov    rax, QWORD PTR [rdi]
       mov    QWORD PTR [rsi], rax
   .L3:
       mov    rax, QWORD PTR [rsi]
       lock cmpxchg    QWORD PTR [rdx], rsi
       mov    QWORD PTR [rsi], rax !!!!!!!!!!!!!!!!!!!!!!!
       jne    .L3
       rep; ret

clang 3.3 [ассемблер]

       movq    (%rdi), %rcx
       movq    %rcx, (%rsi)
   .LBB0_1:
       movq    %rcx, %rax
       lock
       cmpxchgq    %rsi, (%rdi)
       movq    %rax, (%rsi) !!!!!!!!!!!!!!!!!!!!!!!
       cmpq    %rcx, %rax !!!!!!!!!!!!!!!!!!!!!!!
       movq    %rax, %rcx
       jne    .LBB0_1
       ret

icc 13.0.1 [ассемблер]

       movl      %edx, %ecx
       movl      (%rsi), %r8d
       movl      %r8d, %eax
       lock
       cmpxchg   %ecx, (%rdi)
       movl      %eax, (%rsi) !!!!!!!!!!!!!!!!!!!!!!!
       cmpl      %eax, %r8d !!!!!!!!!!!!!!!!!!!!!!!
       je        ..B1.7
   ..B1.4:
       movl      %edx, %ecx
       movl      %eax, %r8d
       lock
       cmpxchg   %ecx, (%rdi)
       movl      %eax, (%rsi) !!!!!!!!!!!!!!!!!!!!!!!
       cmpl      %eax, %r8d !!!!!!!!!!!!!!!!!!!!!!!
       jne       ..B1.4
   ..B1.7:
       ret

Visual Studio 2012 [Нет необходимости проверять ассемблер, MS использует _InterlockedCompareExchange!!!]

   inline int _Compare_exchange_seq_cst_4(volatile _Uint4_t *_Tgt, _Uint4_t *_Exp, _Uint4_t _Value)
   {    /* compare and exchange values atomically with
       sequentially consistent memory order */
       int _Res;
       _Uint4_t _Prev = _InterlockedCompareExchange((volatile long
*)_Tgt, _Value, *_Exp);
       if (_Prev == *_Exp) !!!!!!!!!!!!!!!!!!!!!!!
           _Res = 1;
       else
       { /* copy old value */
           _Res = 0;
           *_Exp = _Prev;
       }
       return (_Res);
   }

Ответ 3

[...]

разорвать циклы CAS, такие как Параллелизм в действии, листинг 7.2:

while(!head.compare_exchange_weak(new_node->next, new_node);

Спецификация (29.6.5 [atomics.types.operations.req]/21-22), кажется, подразумевают, что результат сравнения должен быть частью атомного Работа:

[...]

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

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

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

Атомно, сравнивает содержимое памяти, на которую указывает объект или этим для равенства с тем, что в ожидаемом, и если истина, заменяет содержимое памяти, на которое указывает объект или тем самым в желаемом, и если false, обновляет содержимое памяти в ожидается с содержимым памяти, указанным объектом или это. [n4140 § 29.6.5/21] (N.B. Формулировка остается неизменной между C++ 11 и C++ 14)

Проблема заключается в том, что кажется, что фактический язык стандарта сильнее первоначальной цели предложения. Херб Саттер говорит, что использование Concurrency in Action никогда не предназначалось для поддержки, а обновление expected предназначалось только для локальных переменных.

Я не вижу каких-либо текущих сообщений об ошибках по этому вопросу. [см. второе обновление ниже] Если на самом деле этот язык сильнее, чем предполагалось, то, вероятно, один будет подан. Либо формулировка C++ 11 будет обновлена, чтобы гарантировать ожидаемое поведение вышеприведенного кода, тем самым делая текущие реализации несовместимыми, либо новая формулировка не будет гарантировать такое поведение, в результате чего приведенный выше код потенциально может привести к неопределенному поведению. В этом случае я думаю, что Энтони книгу нужно будет обновить. Что комитет будет делать по этому поводу и соответствует ли фактическая реализация первоначальному замыслу (а не фактической формулировке спецификации), все еще остается открытым вопросом. [см. обновление ниже]

В то же время, для написания кода вам нужно будет учитывать реальное поведение реализации, независимо от того, соответствует оно или нет. Существующие реализации могут быть "ошибочными" в том смысле, что они не реализуют точную формулировку спецификации ISO, но они работают так, как предполагали их реализации, и их можно использовать для написания поточно-безопасного кода. [см. обновление ниже]

Итак, чтобы ответить на ваши вопросы напрямую:

но действительно ли это осуществимо?

Я полагаю, что фактическая формулировка спецификации не является разумно реализуемой (и что фактическая формулировка делает гарантии более сильными, чем обеспечивает библиотека Энтони just::thread. Например, реальная формулировка, кажется, требует атомарных операций над неатомарным объектом. Энтони слегка более слабая интерпретация того, что присвоение expected не обязательно должно быть атомарным, а должно быть обусловлено неудачей обмена, очевидно, реализуема. Трава даже более слабая интерпретация также, очевидно, реализуема, как то, что на самом деле большинство библиотек на самом деле внедрить. [см. обновление ниже]

Is std::atomic_compare_exchange_weak thread-unsafe by design?

Операция не является небезопасной, независимо от того, делает ли она гарантии такими же сильными, как фактическая формулировка спецификации, или слабыми, как указывает Херб Саттер. Это просто, что правильное, потокобезопасное использование операции зависит от того, что гарантировано. Пример кода из Concurrency in Action - небезопасное использование compare_exchange, которое предлагает только слабую гарантию Herb, но может быть написано для корректной работы с реализацией Herb. Это можно сделать так:

node *expected_head = head.load();
while(!head.compare_exchange_weak(expected_head, new_node) {
  new_node->next = expected_head;
}

С этим изменением "ложные" записи в expected просто производятся в локальную переменную и больше не приводят к гонкам. Запись в new_node->next теперь зависит от того, произошел ли обмен, и поэтому new_node->next не виден ни для какого другого потока и может быть безопасно обновлен. Этот пример кода является безопасным как при текущих реализациях, так и при более строгих гарантиях, поэтому он должен быть ориентирован на будущее в отношении любых обновлений атомарных элементов C++ 11, которые решают эту проблему.


Обновление:

Фактические реализации (по крайней мере, MSVC, gcc и clang) были обновлены, чтобы предложить гарантии под интерпретацией Энтони Уильямса; то есть они прекратили изобретать записи в expected в случае успеха обмена.

https://llvm.org/bugs/show_bug.cgi?id=18899

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=60272

https://connect.microsoft.com/VisualStudio/feedback/details/819819/std-atomic-compare-exchange-weak-has-spurious-write-which-can-cause-race-conditions

Обновление 2:

Отчет о дефекте по этому вопросу был подан в комитет C++. Исходя из предложенной в настоящее время резолюции, комитет хочет сделать более строгие гарантии, чем те, что были проверены в реализациях, которые вы проверили (но не так сильно, как нынешняя формулировка, которая, как представляется, гарантирует атомарные операции на неатомарных объектах.) Проект следующего стандарта C++ (C++ 1z или 'C++ 17') еще не приняли улучшенную формулировку.

Обновление 3: C++ 17 принял предлагаемую резолюцию.

Ответ 4

Цитата: Duncan Forster на странице:

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

Итак, есть одна инструкция - (атомная) CAS - которая фактически работает с памятью, а затем другая инструкция для преобразования (атомически назначенного) результата в ожидаемое логическое значение.

Так как значение в %rax было задано атомарно и на него не может воздействовать другой поток, здесь нет расы.

Цитата в любом случае неверна, так как ZF также устанавливается в зависимости от результата CAS (т.е. возвращает как старое значение, так и логическое значение). Тот факт, что флаг не используется, может быть пропущенной оптимизацией, или cmpq может быть быстрее, но это не влияет на правильность.


Для справки рассмотрим разложение compare_exchange_weak как этот псевдокод:

T compare_exchange_weak_value(atomic<T> *obj, T *expected, T desired) {
    // setup ...
    lock cmpxchgq   %rcx, (%rsp) // actual CAS
    return %rax; // actual destination value
}

bool compare_exchange_weak_bool(atomic<T> *obj, T *expected, T desired) {
    // CAS is atomic
    T actual = compare_exchange_weak_value(obj, expected, desired);
    // now we figure out if it worked
    return actual == *expected;
}

Согласны ли вы, что CAS является атомарным?


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

Для справки, вы можете обойти это, заставив безусловный магазин в локальный и снова создав потенциально видимое хранилище:

struct node {
  int data;
  node* next;
};

std::atomic<node*> head;

void push(int data) {
  node* new_node = new node{data};
  node* cur_head = head.load(std::memory_order_relaxed);
  do {
    new_node->next = cur_head;
  } while (!head.compare_exchange_weak(cur_head, new_node,
            std::memory_order_release, std::memory_order_relaxed));
}

Ответ 5

Эти люди, похоже, не понимают ни стандарта, ни инструкций.

Прежде всего, std::atomic_compare_exchange_weak не небезопасно по дизайну. Это полная бессмыслица. Конструкция очень четко определяет, что делает функция и какие гарантии (включая атомарность и порядок памяти) она должна обеспечить.
Независимо от того, какая ваша программа, использующая эту функцию, поточно-безопасная в целом - это другое дело, но семантика функции сама по себе, безусловно, правильна в смысле атомарного обмена copare (вы все равно можете писать небезопасный код с использованием любой доступной нити - безопасный примитив, но это совершенно другая история).

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

Во-вторых, код, созданный существующими реализациями, не имеет отношения к правильности или безопасности потоков std::atomic_compare_exchange_weak. В лучшем случае, если сгенерированные инструкции работают неправильно, это проблема реализации, но она не имеет ничего общего с конструкцией языка. Стандарт языка определяет, какое поведение должно быть реализовано в реализации, оно не отвечает за правильность его выполнения.

В-третьих, в сгенерированном коде нет проблем. Команда x86 CMPXCHG имеет четко определенный режим работы. Он сравнивает фактическое значение с ожидаемым значением, и если сравнение выполняется успешно, оно выполняет своп. Вы знаете, была ли успешна операция, если посмотреть на EAX (или RAX в x64) или на состояние ZF.
Важно то, что атомный обмен-обмен является атомарным, и этот случай. Независимо от того, что вы делаете после этого, результат не должен быть атомарным (в вашем случае, CMP), так как состояние больше не изменяется. Либо обмен был успешным на тот момент, либо он потерпел неудачу. В любом случае это уже "история".

std::atomic_compare_exchange_weak имеет разную семантику, чем базовая команда, она возвращает значение bool. Поэтому вы не всегда можете ожидать сопоставления 1:1 с инструкциями. Компилятору, возможно, придется генерировать дополнительные инструкции (и разные в зависимости от того, как вы потребляете результат) для реализации этой семантики, но это действительно не имеет никакого значения для правильности.

Единственное, на что можно было бы пожаловаться, это тот факт, что вместо прямого использования уже существующего состояния ZFJcc или CMOVcc) он выполняет другое сравнение. Но это проблема производительности (1 цикл впустую), а не проблема корректности.