Оптимистичное чтение и блокировка STM (программной транзакционной памяти) с помощью C / C ++

Я проводил некоторые исследования реализаций STM (программной транзакционной памяти), в частности алгоритмов, которые используют блокировки и не зависят от наличия сборщика мусора, чтобы поддерживать совместимость с неуправляемыми языками, такими как C / C ++. Я прочитал главу STM в книге Херлихи и Шавит «Искусство Многопроцессорное программирование », а также прочитать пару статей Шавита, в которых описывается его " Transactional Locking " и " Transactional Locking II " Реализации STM. Их базовый подход заключается в использовании хеш-таблицы, в которой хранятся значения глобальных часов версии и блокировки, чтобы определить, была ли затронута область памяти при записи другого потока. Насколько я понимаю алгоритм, когда выполняется транзакция записи, часы версии считываются и сохраняются в локальной памяти потока, а набор чтения и записи также создаются в локальной памяти потока. Затем выполняются следующие шаги:

  1. Значения любых прочитанных адресов сохраняются в наборе чтения. Это означает, что транзакция проверяет, что любые считываемые ячейки не заблокированы, и что они равны или меньше локально сохраненного значения часов версии.
  2. Значения любых записанных адресов сохраняются в наборе для записи вместе со значениями, которые должны быть записаны в эти места.
  3. Как только вся транзакция записи завершена (и это может включать чтение и запись в несколько ячеек), транзакция пытается заблокировать каждый адрес, который должен быть записан, с помощью блокировки в хеш-таблице, которая хешируется по адресу ' ценность.
  4. Когда все адреса, установленные для записи, заблокированы, часы глобальной версии автоматически увеличиваются, а новое увеличенное значение сохраняется локально.
  5. Запись-транзакция снова проверяет, чтобы убедиться, что значения в наборе для чтения не были обновлены с новым номером версии или не заблокированы другим потоком.
  6. Транзакция записи обновляет отметку версии для этой ячейки памяти с новым значением, которое она сохранила с шага # 4, и фиксирует значения в наборе записи в память.
  7. Блокировки ячеек памяти снимаются.

Если какой-либо из вышеуказанных шагов проверки завершился неудачно (то есть шаги №1, №3 и №5), то транзакция записи прерывается.

Процесс чтения-транзакции намного проще. Согласно документам Шавита, мы просто

  1. Чтение и локальное сохранение глобального значения часов версии
  2. Убедитесь, что в ячейках памяти значение часов не превышает текущее сохраненное глобальное значение часов версии, а также убедитесь, что ячейки памяти в настоящее время не заблокированы.
  3. Выполните операции чтения
  4. Повторите шаг № 2 для проверки

Если один из шагов №2 или №4 терпит неудачу, то транзакция чтения прерывается.

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

Thread A выполняет следующее внутри транзакции атомарного чтения: linked_list_node* next_node = node->next;

Thread B выполняет следующее: delete node;

Поскольку next_node является локальной переменной потока, это не транзакционный объект. Операция разыменования, необходимая для присвоения ему значения node->next, на самом деле требует двух отдельных чтений. В промежутках между этими чтениями delete может вызываться node, так что чтение из члена next фактически считывается из сегмента памяти, который уже был освобожден. Поскольку чтения оптимистичны, освобождение памяти, на которое указывает node в Thread B, не будет обнаружено в Thread A. Не вызовет ли это возможный сбой или ошибку сегментации? Если да, то как этого можно избежать без блокировки ячеек памяти для чтения (то, что отмечается как в учебнике, так и в документах, не является необходимым)?


person Jason    schedule 08.12.2011    source источник
comment
В самом деле, это странно, было бы здорово, если бы вы могли привлечь внимание @jalf, его магистерская диссертация была посвящена STM, поэтому, надеюсь, он сможет либо объяснить, почему это работает, либо подтвердить, что вы правы. Он ведет блог по адресу: jalf.dk/blog и, возможно, его можно найти в C ++ Lounge.   -  person Matthieu M.    schedule 08.12.2011


Ответы (1)


Простой ответ заключается в том, что delete - это побочный эффект, а транзакции плохо сочетаются с побочными эффектами.

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

Я не думаю, что существует единый универсальный ответ на вопрос «как с этим справиться», но общий подход состоит в том, чтобы отложить вызов delete до времени фиксации. STM API должен либо делать это автоматически (например, предоставлять свою собственную delete функцию и требовать от вас этого), либо давая вам ловушку, в которой вы можете зарегистрировать «действия, выполняемые при фиксации». Затем во время транзакции вы можете зарегистрировать объект, который будет удален, если и когда транзакция будет зафиксирована.

Любая другая транзакция, работающая с удаленным объектом, должна затем не пройти проверку версии и откатиться.

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

person jalf    schedule 08.12.2011
comment
Не оставит ли выполнение перезапуска памяти во время процесса фиксации несогласованное состояние для оптимистичных читателей? Если транзакция чтения не повторно считывает исходный указатель на удаленную ячейку памяти, чтобы увидеть, что она была изменена, не будет ли она продолжать обращаться к памяти, которая была освобождена другой транзакцией записи? Возможным решением будет использование отдельных блокировок чтения вместе с вашим специальным STM-delete методом? Блокировки чтения будут использоваться только во время удаления, чтобы убедиться, что никакая транзакция не ссылается на ячейку памяти, не добавляя накладных расходов в транзакциях записи. - person Jason; 08.12.2011
comment
Ага, ты прав. По этой причине я не использую оптимистичных читателей в своей реализации. Он может работать на управляемом языке, таком как Java или C # (потому что тогда ошибка может быть обнаружена и транзакция чтения откатывается), но в C / C ++ это приведет вас прямо к неопределенному поведению. В своей реализации я использую своего рода облегченное отслеживание читателей, поэтому объект никогда не изменяется на месте, пока читатели обращаются к нему. - person jalf; 08.12.2011
comment
Короче говоря, я использую своего рода двойную буферизацию. Каждый транзакционный объект фактически хранит две копии самого себя, своего рода передний и задний буфер (если заимствовать терминологию из графики). Читатели всегда обращаются к переднему буферу, а фиксации записываются в задний буфер. После фиксации буферы переворачиваются. Кроме того, я отслеживаю количество считывателей, которые обращались к каждому буферу, и поэтому я запрещаю перевороты буфера, если количество считывателей в заднем буфере не равно нулю. - person jalf; 08.12.2011
comment
На практике это позволяет избежать проблем с оптимистичными читателями (поскольку объекты гарантированно останутся неизменными, пока их использует кто-нибудь), при этом минимизируя снижение производительности, которое это вызывает (я все еще могу зафиксировать, даже если транзакции читают передний буфер, я просто не могу зафиксировать дважды, пока существующие читатели не освободят объект) - person jalf; 08.12.2011
comment
Но да, схемы, основанные на оптимистичных читателях на C или C ++, в основном не работают. Невозможно безопасно восстановить данные, если считыватели отключены какой-то транзакцией, изменяющей данные, которые они используют. - person jalf; 08.12.2011
comment
Спасибо, что нашли время, чтобы ответить на мой вопрос ... это было очень поучительно, поскольку многие исследовательские работы, кажется, замалчивают эти проблемы с оптимистично настроенными читателями, и мне было интересно, было ли что-то очевидное, чего мне не хватало, что все остальные в исследовательском сообществе понимается как некий фундаментальный факт. Кстати, мне было бы интересно прочитать вашу диссертацию в какой-то момент. Ссылка в вашем блоге по-прежнему выдает ошибку 404, но не торопитесь :-) - person Jason; 08.12.2011
comment
Что ж, может быть какой-то секретный трюк, который заставляет все это работать с оптимистично настроенными читателями, но я об этом не знаю. Я думаю, причина, по которой это замалчивается, заключается в том, что (1) многие исследователи работают с Java, с которой можно безопасно работать, и (2) те, кто работает на C, не особо беспокоятся о мельчайших деталях. . - person jalf; 08.12.2011
comment
На практике вы можете часто получить это, если обрабатываете segfaults и проверяете транзакцию чтения при фиксации, и данные, которые вы читаете, не не меняются произвольно (если удаленная память все еще сохраняет свое прежнее значение, ваш tx вряд ли выйдет из строя безвозвратно. И я исправлю ссылку, как только у меня будет 5 минут. Спасибо, что указали на это :) - person jalf; 08.12.2011