Назначение через копирование и обмен против двух блокировок

Заимствуем пример Говарда Хиннанта и модифицируем его для использования копирования и замены , это op= потокобезопасный?

struct A {
  A() = default;
  A(A const &x);  // Assume implements correct locking and copying.

  A& operator=(A x) {
    std::lock_guard<std::mutex> lock_data (_mut);
    using std::swap;
    swap(_data, x._data);
    return *this;
  }

private:
  mutable std::mutex _mut;
  std::vector<double> _data;
};

Я считаю, что это потокобезопасно (помните, что параметр op= передается по значению), и единственная проблема, которую я могу найти, это та, что скрыта под ковром: ctor копирования. Однако это будет редкий класс, который допускает копирование-присваивание, но не копирование-конструкцию, так что проблема одинаково существует в обоих вариантах.

Учитывая, что самоназначение настолько редко (по крайней мере, для этого примера), что я не возражаю против дополнительной копии, если это произойдет, считайте потенциальную оптимизацию этого != &rhs либо незначительной, либо пессимизацией. Есть ли какая-либо другая причина предпочесть или избегать ее по сравнению с исходной стратегией (ниже)?

A& operator=(A const &rhs) {
  if (this != &rhs) {
    std::unique_lock<std::mutex> lhs_lock(    _mut, std::defer_lock);
    std::unique_lock<std::mutex> rhs_lock(rhs._mut, std::defer_lock);
    std::lock(lhs_lock, rhs_lock);
    _data = rhs._data;
  }
  return *this;
}

Между прочим, я думаю, что это кратко обрабатывает ctor копирования, по крайней мере, для этого класса, даже если это немного бестолково:

A(A const &x) : _data {(std::lock_guard<std::mutex>(x._mut), x._data)} {}

person Fred Nurk    schedule 21.02.2011    source источник
comment
Ответ побудил меня задать этот вопрос.   -  person Fred Nurk    schedule 22.02.2011
comment
Как первоначальный автор связанного ответа и пришедший к тому же решению (используя передачу по значению, чтобы избежать двойной блокировки), я чувствую, что должен указать на принципиальное различие между двумя подходами, которое оказывается слишком длинным для комментария , поэтому я редактирую ответ.   -  person David Rodríguez - dribeas    schedule 24.02.2011


Ответы (1)


Я считаю, что ваше задание является потокобезопасным (при условии, конечно, что нет ссылок вне класса). Производительность этого варианта по сравнению с вариантом const A&, вероятно, зависит от A. Я думаю, что для многих A ваша перезапись будет такой же быстрой, если не быстрее. Большой контрпример, который у меня есть, это std::vector (и подобные ему классы).

std::vector имеет емкость, которая не участвует в его значении. И если левый имеет достаточную емкость по сравнению с правым, то повторное использование этой емкости вместо того, чтобы выбрасывать ее во временную, может привести к выигрышу в производительности.

Например:

std::vector<int> v1(5);
std::vector<int> v2(4);
...
v1 = v2;

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

Отмечу, что с точки зрения безопасности потоков оба алгоритма блокируют/разблокируют две блокировки. Хотя вариант подкачки позволяет избежать необходимости блокировать их оба одновременно. Я считаю, что в среднем стоимость блокировки обоих одновременно невелика. Но в сильно оспариваемых случаях использования это может стать проблемой.

person Howard Hinnant    schedule 21.02.2011
comment
Я запутался в столь же быстрой части, я думал, что вся идея идиомы копирования и подкачки была основана на исключении копирования параметра, не потерпит ли это неудачу в этом случае? Я имею в виду, что когда copy-ctor блокируется, я был бы удивлен, если бы компилятор мог игнорировать копию, таким образом, всегда создавая истинную копию объекта, который мы хотим скопировать. И когда создается настоящая копия в куче, она всегда будет медленнее, чем первоначальное назначение. Или я ошибаюсь? - person Roman L; 22.02.2011
comment
Копию все же можно опустить, если аргументом оператора присваивания копии является значение rvalue. И это все еще потокобезопасно! :-) Когда у вас есть ссылка на rvalue, вы уверены, что никто другой, даже другой поток, не имеет ссылки на то же rvalue. Таким образом, безопасно делать с этим rvalue все, что вы хотите. Единственным исключением из того, что я говорю, является случай, когда кто-то приводит lvalue к rvalue. Но в этом случае заклинатель несет ответственность за то, чтобы программа действительно могла обрабатывать преобразованное значение l как значение r, даже в многопоточной среде. - person Howard Hinnant; 22.02.2011
comment
Да, под двумя блокировками в заголовке я имел в виду одновременное (что выглядит как общий источник проблем, подобно тому, как часто упускают из виду самоназначение?). Я думал о rvalue-refs и других подобных вещах (хотя выше нет конструктора перемещения) и полностью упустил существующую емкость. Спасибо. - person Fred Nurk; 22.02.2011
comment
Если это поможет, вот реализация блокировки двух блокировок одновременно: llvm.org/svn/llvm-project/libcxx/trunk/include/mutex найдите шаблон ‹class _L0, class _L1› void lock(_L0& __l0, _L1& __l1) (я боюсь, как это будет отформатировано в комментарий). Это открытый исходный код, так что не стесняйтесь его использовать, но, пожалуйста, сохраняйте авторские права вместе с исходным кодом, где бы вы это ни делали (это гораздо более щедро, чем GPL3). - person Howard Hinnant; 22.02.2011
comment
@ Ховард, спасибо за объяснение! Я понял, что причиной моего замешательства на самом деле было наличие общих данных (доступ к которым заблокирован в копи-кторе), которые не являются rvalue, даже если сам объект есть. Я забыл, что это, однако, не считается, и копии все равно можно исключить, даже если это может изменить поведение программы (эта часть всегда была для меня сложной). - person Roman L; 22.02.2011
comment
@ Ховард, но теперь мне любопытно - не означает ли то, что вы говорите, что копирование и обмен всегда будет неэффективным, когда некоторые члены данных (или члены членов и т. Д.) std::vector-подобны, независимо от многопоточности ? Что, наконец, означает, что копирование и замена не должны использоваться, когда эта пессимизация потенциально возможна, то есть во многих случаях? - person Roman L; 22.02.2011
comment
@7vies: Ваш вывод частично верен. Автор каждого класса должен определить наилучший алгоритм назначения копии. Иногда это будет копировать-конструировать-и-заменять, а иногда и нет. Присутствие вектороподобных членов может повлиять на это решение, а может и нет. Это будет зависеть от всех членов/баз, а также может зависеть от наиболее вероятных вариантов использования оператора присваивания копии. Там просто не обойтись без тестирования производительности. :-) - person Howard Hinnant; 22.02.2011
comment
И это также может зависеть от деталей реализации векторной реализации, которую вы используете. :-( Извините, нет простого ответа, который вы можете взять с собой в банк. За исключением, возможно, того, что шаблоны - это ваши инструменты, а не ваш мастер. - person Howard Hinnant; 22.02.2011
comment
Ясно... насколько я помню, копирование и замена иногда представлялись как серебряная пуля, теперь я буду знать, что это действительно зависит. Еще раз спасибо! - person Roman L; 22.02.2011
comment
@7vies: Хотя производительность может иметь значение, я обычно реализую назначение как копирование и обмен, потому что это проще. Поскольку это деталь реализации, я всегда могу вернуться и изменить ее, если это необходимо, но в то же время, по крайней мере, мой код правильный. - person Matthieu M.; 22.02.2011
comment
@Matthieu: Да, это частный случай общего правила: сначала KISS, а затем при необходимости оптимизируйте. Тем не менее, важно знать последствия. - person Roman L; 22.02.2011
comment
Кстати, в этом ответе отсутствует то, что семантика одновременной блокировки обоих мьютексов по сравнению с копированием и обменом не совсем одинакова, и в процессе копирования и обмена существует потенциальное состояние гонки (которое вы могли бы ну, не беспокойтесь об этом), так как правая сторона может быть изменена между моментом ее копирования в аргумент и моментом получения блокировки левой стороны. Блокировка обоих мьютексов гарантирует, что в какой-то момент времени и левый, и правый будут иметь одинаковое значение, копирование и обмен предлагает меньшую гарантию того, что после присваивания левый имеет значение, которое было у правого в определенный момент времени. - person David Rodríguez - dribeas; 24.02.2011
comment
Если в вашем приложении потокобезопасность зависит только от согласованности каждого элемента данных, то оба являются потокобезопасными. Одним из приложений для этого может быть несколько потоков, выполняющих операции с высокой пропускной способностью, один поток, собирающий статистику: вам может быть все равно, является ли статистика точным снимком статистики во всех потоках, поскольку к тому времени, когда пользователь получит значения, они уже изменятся, результат является только оценкой. Но важно, чтобы статистика, считываемая из каждого потока, была согласованной, в частности, в 32-битном ящике, если статистика представляет собой 64-битное целое число, отсутствие блокировки может вызвать ошибку 2^32! - person David Rodríguez - dribeas; 24.02.2011
comment
@DavidRodríguez-dribeas: Это немного другая гарантия, но она не кажется менее точной. - person Fred Nurk; 14.03.2011
comment
@Fred, я думаю, это подлежит обсуждению, но один гарантирует, что копия и оригинал будут иметь одинаковую ценность в какой-то конкретный момент времени, а другой гарантирует, что копия будет иметь ценность, которую оригинал имел в какое-то время. Таким образом, первое из них сильнее: оно гарантирует как согласованность со значением, которое имел источник, так и то, когда эта согласованность возникает. Другой вариант кажется мне менее, так как хотя он гарантирует, что в копии нет мусора, в то же время он не гарантирует согласованности в конкретный момент времени. Но опять же, это предмет обсуждения. Наверняка это разные гарантии. - person David Rodríguez - dribeas; 14.03.2011
comment
@DavidRodríguez-dribeas: Разве копирование и замена также не определяет, когда возникает эта согласованность? А именно, значение при запуске присваивания (поскольку мы не рассматриваем присваивание как атомарное). Ни в том, ни в другом случае вы не могли бы гарантировать, например: a = b; assert(a == b); - person Fred Nurk; 14.03.2011
comment
@Fred Nurk: С идиомой копирования и подкачки вы не можете утверждать, что с помощью механизма двойной блокировки вы можете утверждать это до снятия блокировки. Я думаю, что предоставил пример кода, в котором можно увидеть, что поведение обоих отличается, я мог бы посмотреть его, но, возможно, было бы быстрее переписать его. Поток выполняет a = b; в тесном цикле, другой поток работает: std::lock( a.m, b.m ); ++a; ++b; assert( equals_non_locking(a,b) ); В версии с копированием и заменой вы в конечном итоге столкнетесь с ситуацией, когда копирование выполняется до приращений, а затем a будет отставать: утверждение не удастся. - person David Rodríguez - dribeas; 14.03.2011
comment
@DavidRodríguez-dribeas: Но блокировка удерживается внутри op=, поэтому вы не можете гарантировать a = b; assert(a == b); из любого другого кода. Ваш пример неполный, и я не вижу (как только я закончу его, как кажется разумным), как версия CaS может потерпеть неудачу в утверждении; не могли бы вы завершить его? (В частности, кажется, что вы хотите сохранить блокировку a.m и b.m на протяжении всего приращения и утверждения. Поскольку они оба заблокированы, другой поток не может ничего делать с «a» или «b» в этом регионе.) - person Fred Nurk; 14.03.2011