Использовать atomic вместо CriticalSection?

У меня есть код, который дает некоторые данные, выполняет некоторые операции с файлами и т. Д. Я использую 1 поток для передачи данных и 4 потока для чтения и поиска внутри данных.

4 потока используют вектор файлов для поиска (используя ту же функцию). Чтобы избежать проблем с синхронизацией (все потоки читают один и тот же файл одновременно), я использую CriticalSection() WinAPI:

void ReadData(char* fileName)
{
   EnterCriticalSection(&CriticalSection);

   // Open file

   // Read file data
   std::vector<std::string> data;

   ... Find data inside file

   // Close file

   LeaveCriticalSection(&CriticalSection);
}

Но я видел это Post и это:

Объекты атомарных типов — единственные объекты C++, не подверженные гонкам данных; то есть, если один поток записывает в атомарный объект, а другой поток читает из него, поведение четко определено.

Мой вопрос: лучше использовать std::atomic вместо CriticalSection? или я не понимаю, как использовать атомарность.


person Ali Sepehri-Amin    schedule 27.10.2017    source источник
comment
если доступ к памяти можно сделать как заблокированный, нет необходимости использовать критическую секцию или другие синхронизации здесь. однако далеко не все можно сделать только заблокированными операциями. вам нужно понять, что вы делаете и что вам нужно   -  person RbMm    schedule 27.10.2017
comment
Btw критические разделы не работают на Windws, так как блокировка может завершиться ошибкой с исключением SEH. Вам следует рассмотреть возможность использования блокировок SRW, введенных в NT 6.x (начиная с Vista). Они занимают меньше места и могут быть инициализированы статически. Начиная с Visual Studio 2015 std::mutex основан на блокировках SRW. В предыдущих версиях std::mutex сильно ломался и мог вызывать взаимоблокировки. Например, если вы вызовете std::mutex::lock() в первый раз из DllMain, он заблокируется.   -  person StaceyGirl    schedule 27.10.2017
comment
@Ivan - критические разделы не сломаны в Windows, а блокировка никогда не дает сбоев и не вызывает исключений.   -  person RbMm    schedule 27.10.2017
comment
std::mutex::lock() впервые из DllMain заблокируется - правда? всегда и безоговорочно? причина взаимоблокировки не мьютекс. тупик вызывает неграмотность. если ждать на критической секции (в широком смысле) внутри другой критической секции - конечно может быть тупиковая ситуация. но это не значит что что-то сломалось. абсолютно нет   -  person RbMm    schedule 27.10.2017
comment
@RbMm кажется, что в какой-то момент они исправили критические разделы, добавив резервный путь. Тем не менее блокировки SRW являются единственной возможной реализацией для std::mutex для C++11 (поскольку вам нужно, чтобы его конструктор был constexpr, который все еще не работает, по крайней мере, на VS2015). Насчет тупика: да, всегда и безоговорочно. И я знаю, что причина взаимоблокировки заключается в том, что никто не ожидает, что блокировка мьютекса заблокирует другой дополнительный мьютекс внутри. Так что да, std::mutex не работает в VS до 2015 года. Как и условные переменные.   -  person StaceyGirl    schedule 27.10.2017
comment
@Ivan - критические разделы в Windows очень старые, и их внутренняя реализация много раз менялась. но я никогда не слышал о каких-то проблемах с ним. и это никогда не вызывает никаких исключений. а насчёт да, всегда и безоговорочно - это неверно. никто не ожидает, что блокировка мьютекса внутренне заблокирует другой дополнительный мьютекс - нонсенс. Можете ли вы объяснить, в каком потоке ожидания объекта возникает взаимоблокировка (как вы говорите)? как критическая секция загрузчика связана с этим?   -  person RbMm    schedule 27.10.2017
comment
@ssbssa - это в самом крайнем случае, когда в Windows так мало памяти, что даже объект события не может быть создан. думаю почти никогда не было на практике. а для блокировки мьютекса внутри блокировки загрузчика *всегда и безоговорочно* конечно смысла нет и не верно   -  person RbMm    schedule 27.10.2017
comment
Реализация @RbMm std::mutex в VS2013 и более ранних версиях основана на некоторой среде выполнения Concurrency, которая инициализируется лениво. По сути, он попытается выполнить какую-то хитрую инициализацию и зависнет где-то в RegisterWaitForSingleObject. Имейте в виду, что все это можно сделать с помощью одного потока в процессе и мьютекса, созданного в стеке - в пользовательском коде нет взаимоблокировки.   -  person StaceyGirl    schedule 27.10.2017
comment
Вы можете не выделить объект ядра не только из-за нехватки памяти, но и из-за ограничения на то, сколько может иметь процесс HANDLE. Я не уверен, каковы текущие ограничения, но если ваша программа создает много объектов ядра, вы можете столкнуться с ошибкой, даже если у вас много свободной памяти. Теперь с тем, что говорит @ssbssa, похоже, что они исправили критические разделы в Vista.   -  person StaceyGirl    schedule 27.10.2017
comment
@Ivan - да, у каждого процесса есть ограничение (очень высокое) для дескрипторов, памяти и т. д. (в зависимости от квоты), но это бывает очень и очень редко. и реализация много раз изменена. start from Vista использовала не событие, а обычное событие с ключом в Windows. начиная с 8.1 используйте NtWaitForAlertByThreadId, которые не создают дополнительных объектов   -  person RbMm    schedule 27.10.2017
comment
@RbMm Я проверил в Windows 7, и они все еще вызывают NtCreateEvent внутри, так что похоже, что они только что добавили запасной путь, но все еще используют события в большинстве ситуаций.   -  person StaceyGirl    schedule 27.10.2017
comment
@Ivan - да, в win7 создать событие. если это не удается - попробуйте использовать событие с ключом   -  person RbMm    schedule 27.10.2017
comment
std::mutex не сломан. Как указано в рекомендациях по работе с DLL внутри DllMain никогда не следует синхронизировать с другими потоками. Это может привести к взаимоблокировке.. Это связано с тем, что самые простые API-интерфейсы, такие как GetModuleFileName, получают блокировку загрузчика, которую уже удерживает DllMain.   -  person rustyx    schedule 27.10.2017
comment
@Ivan на win7 действительно был deadlock при вызове RegisterWaitForSingleObject до завершения инициализации процесса (до точки входа exe), потому что этот API ожидает запуска рабочего потока, но рабочий поток ожидает завершения инициализации процесса. Эта ошибка исправлена ​​в win8.1. но в любом случае не безусловный (если dll загрузить позже, при инициализации процесса - тупика нет). но src - ошибка попробуйте подождать внутри блокировки загрузчика   -  person RbMm    schedule 27.10.2017


Ответы (2)


Используйте стандартный С++

В своем коде вы используете EnterCriticalSection() и LeaveCriticalSection()< /a> функции Microsoft WinAPI.

У них есть несколько серьезных неудобств: во-первых, они не переносимы, во-вторых, они небезопасны: что произойдет, если исключение заставит поток покинуть ReadData() неожиданным образом? Вы можете в конечном итоге получить критический раздел, который кажется окнам не оставленным, что приведет к голоданию всех других потоков!

Стандартная альтернатива C++ с использованием lock_guard на mutex, как продемонстрировал Вернер, намного безопаснее: во-первых, она переносима на разные платформы. , но, кроме того, он реализует идиому RAII, которая гарантирует, что в случае неожиданного исключения lock_guard уничтожается при выходе из функции, в результате чего mutex освобождается.

Использование atomic может быть недостаточным

Очень часто люди испытывают искушение использовать атомарные методы, потому что они избегают гонок данных и создают впечатление, что они решат все проблемы синхронизации потоков.

К сожалению, это не так. Как только вы используете несколько атомарных элементов, вы можете сделать предположение об общей согласованности, тогда как на самом деле все может происходить по-другому и вызывать очень неприятные ошибки. Создание безблокировочных алгоритмов и структур данных чрезвычайно сложно и сложно. Поэтому я настоятельно рекомендую прочитать прекрасную книгу Энтони Уильяма C++ concurrency in action: он подробно исследует все связанные аспекты.

Другие альтернативы

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

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

person Christophe    schedule 27.10.2017
comment
Спасибо, во-первых: я также использовал CreateThread для многопоточности, это удобнее для меня, когда я хочу остановить, приостановить или возобновить потоки. Во-вторых: что вы подразумеваете под очередью? Какой хороший контейнер вместо вектора? - person Ali Sepehri-Amin; 27.10.2017
comment
Да, я должен был упомянуть о преимуществах блокировки с точки зрения обработки исключений. Тщательно объяснил - person Werner Erasmus; 28.10.2017

std::atomic не перегружен для вектора. Его также нельзя копировать или перемещать (отсюда и здесь), и поэтому его нельзя использовать как std::vector value_type (или может потребоваться указать вашу собственную специализацию, которую я не пробовал (и не буду) пытаться/редактировать).

Однако, если вы хотите, чтобы ваш код не зависел от платформы, std::mutex отлично подойдет например:

std::mutex myMutex_; //Typically a member or static or in unnamed namespace

void ReadData(char* fileName)
{
   std::lock_guard<std::mutex> guard(myMutex_);
   // Open file

   // Read file data
   std::vector<std::string> data;

   ... Find data inside file

   // Close file

   //... Releases when scoped left...
}

В определенном смысле мой ответ перекликается с предыдущим сообщением.

person Werner Erasmus    schedule 27.10.2017