Как сделать объект в с ++ изменчивым? - PullRequest
0 голосов
/ 29 августа 2018
`struct MyClass {
  ~MyClass() {
    // Asynchronously invoke deletion (erase) of entries from my_map;
    // Different entries are deleted in different threads.
    // Need to spin as 'this' object is shared among threads and 
    // destruction of the object will result in seg faults.
    while(my_map.size() > 0); // This spins for ever due to complier optimization.
  }
  unordered_map<key, value> my_map;
};`

У меня есть вышеупомянутый класс, в котором элементы неупорядоченной карты асинхронно удаляются в деструкторе, и я должен spin / sleep, поскольку объект является общим для других потоков. Я не могу объявить my_map как volatile, так как это приводит к ошибкам компиляции. Что еще я могу сделать здесь? Как мне сказать компилятору, что my_map.size() приведет к 0 в какой-то момент времени. Пожалуйста, не говорите мне, почему / как этот дизайн плох; Я не могу изменить дизайн, поскольку он связан по причине, которую я не могу объяснить, если я не напишу здесь тысячи строк кода.

Редактировать: my_map защищен с использованием версии спин-блокировки. Итак, потоки захватывают спин-блокировку перед удалением записей. Просто while(my_map.size() > 0); был единственным «наивным» вращением, которое я имел в коде. Я преобразовал это, чтобы захватить спин-блокировку и затем проверить размер (в цикле), и это работало. Хотя использование condition_variable было бы правильным способом, мы используем модель асинхронного программирования (например, SEDA), которая связывает нас с , а не использованием любых спящих / вызывающих вызовов.

1 Ответ

0 голосов
/ 29 августа 2018

volatile не является решением этой проблемы. volatile имеет ровно три применения: 1. Доступ к отображенным в памяти устройствам в драйвере, 2. Обработчики сигналов, 3. Использование setjmp.

Читайте следующее, снова и снова, пока оно не впитается. volatile бесполезно в многопоточности.

Наивный спин-блокировка имеет три проблемы:

  1. Компилятору разрешено кэшировать результаты, поэтому вы видите поведение "вращения навсегда".
  2. В классическом случае у вас есть риск возникновения состояния гонки: поток A может проверить переменную блокировки, найти ресурс доступным, но затем получить приоритет перед установкой переменной блокировки. Приходит поток B, который также находит переменную блокировки, показывающую ресурс как доступный, поэтому он блокирует его и начинает доступ к ресурсу. Затем поток A возвращается в исходное состояние, снова блокирует переменную и также обращается к ресурсу.
  3. Возникла проблема с порядком записи данных. Если защищенная переменная записывается, а затем изменяется переменная блокировки, у вас нет никаких гарантий, что другой поток не увидит измененную защищенную переменную, даже если он может также видеть переменную блокировки, утверждая, что она была записана. И компилятор и Внеочередное выполнение на CPU разрешено делать это.

volatile решает только первую из этих проблем, но ничего не делает для решения двух других. С одним предупреждением по умолчанию MSVC на x86 / x64 добавляет ограничение памяти к volatile доступам, даже если это не требуется стандартом. Это решает третью проблему, но не решает вторую.

Единственное решение всех трех из этих проблем заключается в использовании правильных примитивов синхронизации: std::atomic<>, если вам действительно нужно вращать блокировку, предпочтительно std::mutex и, возможно, std::condition_variable для блокировки, которая переводит поток в спящий режим до чего-то интересное бывает.

...