Если я заблокирую std::mutex
, всегда ли я буду получать ограничение памяти? Я не уверен, подразумевает ли это или заставляет вас получить забор.
Обновлять:
Нашел эту ссылку после комментариев RMF.
Если я заблокирую std::mutex
, всегда ли я буду получать ограничение памяти? Я не уверен, подразумевает ли это или заставляет вас получить забор.
Обновлять:
Нашел эту ссылку после комментариев RMF.
Разблокировка мьютекса синхронизируется с блокировкой мьютекса. Я не знаю, какие у компилятора есть варианты реализации, но получается тот же эффект забора.
A[0]
, а затем освобождает мьютекс. Затем ядро B получает мьютекс и считывает A[0]
(до того, как когерентность кэша сможет передать новое значение A[0]
на ядро B). Другими словами, мьютекс принудительно обновляет все ячейки памяти перед возвратом.
- person Mysticial; 24.06.2012
std::mutex
вызовет полный барьер памяти, включающий невременные хранилища. Таким образом, вы не можете обогнать std::mutex
, но вы можете обогнать std::atomic::store(..., std::memory_order_release);
- person Mysticial; 23.05.2019
Насколько я понимаю, это описано в:
1.10 Многопоточные выполнения и гонки данных
Параграф 5:
В библиотеке определен ряд элементарных операций (раздел 29) и операций над мьютексами (раздел 30), специально идентифицированных как операции синхронизации. Эти операции играют особую роль в том, чтобы сделать назначения в одном потоке видимыми для другого. Операция синхронизации в одной или нескольких ячейках памяти является либо операцией потребления, операцией получения, операцией освобождения, либо операцией получения и освобождения одновременно. Операция синхронизации без связанной ячейки памяти является ограничением и может быть либо ограничением получения, либо ограничением освобождения, либо ограничением захвата и освобождения одновременно. Кроме того, существуют упрощенные атомарные операции, которые не являются операциями синхронизации, и атомарные операции чтения-модификации-записи, обладающие особыми характеристиками. [Примечание: Например, вызов, который захватывает мьютекс, будет выполнять операцию захвата в местах, содержащих мьютекс. Соответственно, вызов, освобождающий тот же мьютекс, будет выполнять операцию освобождения в тех же местах. Неформально выполнение операции освобождения для A заставляет предшествующие побочные эффекты в других местах памяти становиться видимыми для других потоков, которые позже выполняют операцию потребления или получения для A. «Расслабленные» атомарные операции не являются операциями синхронизации, хотя, как и операции синхронизации, они не могут участвовать в гонках данных. -конец примечания]
Операция мьютекса (блокировка или разблокировка) для определенного мьютекса M полезна только для любых целей, связанных с синхронизацией или видимостью памяти, если M совместно используется разными потоками и они выполняют эти операции. Мьютекс, определенный локально и используемый только одним потоком, не обеспечивает никакой значимой синхронизации.
[Примечание: оптимизации, которые я здесь описываю, вероятно, не выполняются многими компиляторами, которые могут рассматривать эти операции мьютекса и атомарной синхронизации как «черные ящики», которые нельзя оптимизировать (или даже которые не следует оптимизировать, чтобы сохранить предсказуемость генерации кода). , и некоторые конкретные шаблоны, что является фиктивным аргументом). Я не удивлюсь, если нулевой компилятор сделает оптимизацию даже в более простом случае, но в том, что они легальны, сомнений нет.]
Компилятор может легко определить, что некоторые переменные никогда не используются несколькими потоками (или каким-либо асинхронным выполнением), особенно для автоматической переменной, адрес которой не используется (или ссылка на нее). Такие объекты называются здесь "приватными потоками". (Все автоматические переменные-кандидаты на выделение регистров являются частными потоками.)
Для частного мьютекса потока не требуется генерировать осмысленный код для операций блокировки/разблокировки: нет атомарного сравнения и обмена, нет ограждения и часто вообще не требуется сохранять состояние, за исключением случая «безопасного мьютекса», когда поведение рекурсивной блокировки хорошо определено и должно дать сбой (чтобы последовательность m.lock(); bool locked = m.try_lock();
работала, вам нужно сохранить как минимум логическое состояние).
Это также верно для любых частных атомарных объектов потока: требуется только голый неатомарный тип, и могут выполняться обычные операции (поэтому выборка-добавление 1 становится обычным приращением поста).
Причина, по которой эти преобразования являются законными:
Все объекты синхронизации специфицируются как инструменты для межпотоковой связи: они могут гарантировать, что побочные эффекты в одном потоке видны в другом потоке; они заставляют четко определенный порядок операций существовать не только в одном потоке (последовательный порядок выполнения операций одного потока), но и в нескольких потоках.
Типичным примером является публикация информации с атомарным типом указателя:
Общие данные:
atomic<T*> shared; // null pointer by default
Ветка публикации выполняет:
T *p = new T;
*p = load_info();
shared.store(p, memory_order_release);
Поток-потребитель может проверить, доступны ли данные, загрузив значение атомарного объекта в качестве потребителя:
T *p = shared.load(memory_order_acquire);
if (p) use *p;
(Здесь нет определенного способа ожидания доступности, это простой пример, иллюстрирующий публикацию и потребление опубликованного значения.)
Поток публикации должен установить атомарную переменную только после завершения инициализации всех полей; порядок памяти — это релиз, сообщающий о завершении манипуляций с памятью.
Другим потокам требуется только порядок получения памяти, чтобы «подключиться» к операции освобождения, если она была. Если значение по-прежнему равно нулю, поток ничего не узнал о мире, и получение не имеет смысла; он не может действовать на него. (К тому времени, когда поток проверит указатель и увидит нулевое значение, общая переменная может быть уже изменена. Это не имеет значения, поскольку разработчик посчитал, что отсутствие значения в этом потоке управляемо, иначе он выполнил бы операцию в последовательности.)
Все атомарные операции предназначены для того, чтобы, возможно, блокировать меньше, то есть заканчивать за короткое конечное время, что бы ни делали другие потоки, даже если они застряли. Это означает, что вы не можете зависеть от другого потока, завершившего работу.
На другом конце спектра примитивов взаимодействия потоков мьютексы не содержат значения, которое можно использовать для передачи информации между потоками (*), но они гарантируют, что один поток может войти в последовательность блокировки-операции-разблокировки только после того, как другой поток завершил свою собственную последовательность блокировки-операции-разблокировки.
(*) даже не логическое значение, поскольку использование мьютекса в качестве общего логического сигнала (= двоичного семафора) между потоками специально запрещено
Мьютекс всегда используется в связи с набором общих переменных: защищенные переменные или объекты V; эти V используются для переноса информации между потоками, а мьютекс делает доступ к этой информации хорошо упорядоченной (или сериализованной) между потоками. С технической точки зрения, все, кроме первой операции блокировки мьютекса (на M), пара с предыдущей операцией разблокировки на M:
Семантика блокировки/разблокировки определена на одном М, так что давайте перестанем повторять «на М»; у нас есть потоки A и B. Блокировка B — это получение, которое сочетается с разблокировкой A. Обе операции вместе образуют синхронизацию между потоками.
[Как насчет потока, который часто блокирует M и будет часто повторно блокировать M без какого-либо другого потока, воздействующего на M в это время? Ничего интересного, приобретение по-прежнему сопряжено с выпуском, но A = B, так что ничего не сделано. Разблокировка была упорядочена в том же потоке выполнения, поэтому в данном конкретном случае это бессмысленно, но в целом поток не может сказать, что это бессмысленно. Это даже не является особым случаем языковой семантики.]
Происходит синхронизация между набором потоков T, воздействующих на мьютекс: ни один другой поток не может гарантированно просматривать какие-либо операции с памятью, выполняемые этими T. Обратите внимание, что на практике на большинстве реальных компьютеров, как только изменение памяти попадает в кеш , все ЦП увидят его, если они проверят тот же адрес, благодаря согласованности кеша. Но потоки C/C++(#) не определяются с точки зрения глобально согласованного кеша и не с точки зрения упорядочения, видимого на ЦП, поскольку сам компилятор может предположить, что неатомарные объекты не изменяются произвольным образом программа без синхронизации (процессор не может предположить ничего подобного, поскольку он не имеет представления об атомарных и неатомарных ячейках памяти). Это означает, что гарантия, доступная для системы ЦП/памяти, на которую вы ориентируетесь, обычно недоступна для высокоуровневой модели C/C++. Вы абсолютно не можете использовать обычный код C/C++ в качестве высокоуровневой сборки. ; только обливая свой код volatile (почти везде), вы можете хотя бы смутно приблизиться к сборке высокого уровня (но не совсем).
(#) "Поток/семантика C/C++" не "Семантика потока языка программирования C/C++": C и C++ основаны на одной и той же спецификации для примитивов синхронизации, это не означает, что существует язык С/С++)
Поскольку действие операций с мьютексом на M заключается только в сериализации доступа к некоторым данным потоками, использующими M, ясно, что другие потоки не видят никакого эффекта. С технической точки зрения отношение synchronize with возникает между потоками, использующими одни и те же объекты синхронизации (мьютексы в этом контексте, одни и те же атомарные объекты в контексте атомарного использования).
Даже когда компилятор создает ограничения памяти на языке ассемблера, он не должен предполагать, что операция разблокировки вносит изменения, выполненные до разблокировки, в потоки за пределами набора T.
Это позволяет декомпозировать наборы потоков для анализа программы: если программа работает параллельно, два набора потоков U и V и U и V создаются так, что U и V не могут получить доступ ни к одному общему объекту синхронизации (но они могут получить доступ к общие неатомарные объекты), то вы можете проанализировать взаимодействие U и V отдельно с точки зрения семантики потоков, поскольку U и V не могут обмениваться информацией четко определенными межпотоковыми способами (они все еще могут обмениваться информацией через систему, например, через дисковые файлы, сокеты, для системной разделяемой памяти).
(Это наблюдение может позволить компилятору оптимизировать некоторые потоки без полного анализа программы, даже если некоторые общие изменяемые объекты «извлекаются» через сторонний класс, который имеет статические члены.)
Другой способ объяснить это — сказать, что семантика этих примитивов не протекает: только те потоки, которые участвуют, получают определенный результат.
Обратите внимание, что это верно только на уровне спецификации для операций получения и освобождения, а не последовательно согласованных операций (что является порядком по умолчанию для операций над атомарным объектом, когда вы не указываете порядок памяти): все последовательно согласованные действия (операции над атомарный объект или забор) происходят в четко определенном глобальном порядке. Однако это ничего не значит для независимых потоков, не имеющих общих атомарных объектов.
Порядок операций отличается от порядка элементов в контейнере, где вы действительно можете перемещаться по контейнеру, или в том, что файлы представлены в порядке их имен. Возможны наблюдения только за объектами, а не за порядком операций. Утверждение, что существует четко определенный порядок, означает только то, что значения не кажутся доказуемо измененными назад (по сравнению с некоторым абстрактным порядком).
Если у вас есть два несвязанных набора, которые упорядочены, скажем, целые числа с обычным порядком и слова с лексикографическим порядком), вы можете определить сумму этих наборов как имеющую порядок, совместимый с обоими порядками. Вы можете поставить цифры до, после или чередовать слова. Вы вольны делать, что хотите, потому что элементы в сумме двух множеств не имеют никакого отношения друг к другу, если они не происходят из одного и того же множества.
Можно сказать, что существует глобальный порядок всех операций с мьютексами, он просто бесполезен, как и определение порядка суммы несвязанных наборов.
memory_order_acquire
, согласно главе Atomics (цитируется в ответе @Alok Save), а освобождение - это операция освобождения (на самом объекте мьютекса). Этот ответ не поддерживает утверждение о том, что мьютексы можно удалить. Хотя это правдоподобно, потому что они не определены как acq_rel, поэтому получение может двигаться произвольно раньше, а выпуск произвольно поздно, что позволяет переупорядочивать что угодно с чем угодно. Ваш ответ должен привести этот аргумент или что-то подобное. Тогда у вас есть интересный момент.
- person Peter Cordes; 18.11.2019