Создает ли std::mutex забор?

Если я заблокирую std::mutex, всегда ли я буду получать ограничение памяти? Я не уверен, подразумевает ли это или заставляет вас получить забор.

Обновлять:

Нашел эту ссылку после комментариев RMF.

Многопоточное программирование и видимость памяти


person Tom Kerr    schedule 23.06.2012    source источник
comment
В системе с одним модулем выполнения вам не нужна заграждение ЦП.   -  person curiousguy    schedule 23.05.2019


Ответы (3)


Разблокировка мьютекса синхронизируется с блокировкой мьютекса. Я не знаю, какие у компилятора есть варианты реализации, но получается тот же эффект забора.

person R. Martinho Fernandes    schedule 23.06.2012
comment
Я думаю, что ОП спрашивает об ограждении других областей памяти, кроме самого мьютекса. - person Mysticial; 24.06.2012
comment
Я не понимаю. Заборы в C++ не влияют на определенные области памяти. - person R. Martinho Fernandes; 24.06.2012
comment
Я говорю следующее: предположим, что ядро ​​A записывает в A[0], а затем освобождает мьютекс. Затем ядро ​​B получает мьютекс и считывает A[0] (до того, как когерентность кэша сможет передать новое значение A[0] на ядро ​​B). Другими словами, мьютекс принудительно обновляет все ячейки памяти перед возвратом. - person Mysticial; 24.06.2012
comment
Как и в случае с забором, он заставляет обновлять только некоторые ячейки памяти (те, в которых изменения модели памяти должны быть видны в соответствии с отношением synchronizes with). - person R. Martinho Fernandes; 24.06.2012
comment
Если это поможет, ограничения определяются просто как установление отношений synchronizes with и ничего больше. Все гарантии, которые вы получаете от забора, являются лишь следствием правил модели памяти. Поскольку мьютексы также устанавливают отношения synchronizes with, их эффекты одинаковы (ну, мьютексы имеют другие эффекты, но здесь они неуместны). - person R. Martinho Fernandes; 24.06.2012
comment
@R.MartinhoFernandes Это действительно помогает, ты. Я использую стандартный контейнер и у меня проблемы с размером, что, вероятно, является частью проблемы. Я постараюсь получить простой пример позже вечером. - person Tom Kerr; 24.06.2012
comment
@Mysticial Как вы можете превзойти когерентность кеша? На каком процессоре? - person curiousguy; 23.05.2019
comment
@curiousguy Вы можете на x86 использовать невременные хранилища, если вы полагаетесь на семантику получения/выпуска для синхронизации. Но std::mutex вызовет полный барьер памяти, включающий невременные хранилища. Таким образом, вы не можете обогнать std::mutex, но вы можете обогнать std::atomic::store(..., std::memory_order_release); - person Mysticial; 23.05.2019
comment
@Mysticial Как это гарантируется? - person curiousguy; 24.05.2019

Насколько я понимаю, это описано в:

1.10 Многопоточные выполнения и гонки данных

Параграф 5:

В библиотеке определен ряд элементарных операций (раздел 29) и операций над мьютексами (раздел 30), специально идентифицированных как операции синхронизации. Эти операции играют особую роль в том, чтобы сделать назначения в одном потоке видимыми для другого. Операция синхронизации в одной или нескольких ячейках памяти является либо операцией потребления, операцией получения, операцией освобождения, либо операцией получения и освобождения одновременно. Операция синхронизации без связанной ячейки памяти является ограничением и может быть либо ограничением получения, либо ограничением освобождения, либо ограничением захвата и освобождения одновременно. Кроме того, существуют упрощенные атомарные операции, которые не являются операциями синхронизации, и атомарные операции чтения-модификации-записи, обладающие особыми характеристиками. [Примечание: Например, вызов, который захватывает мьютекс, будет выполнять операцию захвата в местах, содержащих мьютекс. Соответственно, вызов, освобождающий тот же мьютекс, будет выполнять операцию освобождения в тех же местах. Неформально выполнение операции освобождения для A заставляет предшествующие побочные эффекты в других местах памяти становиться видимыми для других потоков, которые позже выполняют операцию потребления или получения для A. «Расслабленные» атомарные операции не являются операциями синхронизации, хотя, как и операции синхронизации, они не могут участвовать в гонках данных. -конец примечания]

person Alok Save    schedule 23.06.2012

Операция мьютекса (блокировка или разблокировка) для определенного мьютекса 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:

  • блокировка M - это операция получения на M
  • разблокировка 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 не могут обмениваться информацией четко определенными межпотоковыми способами (они все еще могут обмениваться информацией через систему, например, через дисковые файлы, сокеты, для системной разделяемой памяти).

(Это наблюдение может позволить компилятору оптимизировать некоторые потоки без полного анализа программы, даже если некоторые общие изменяемые объекты «извлекаются» через сторонний класс, который имеет статические члены.)

Другой способ объяснить это — сказать, что семантика этих примитивов не протекает: только те потоки, которые участвуют, получают определенный результат.

Обратите внимание, что это верно только на уровне спецификации для операций получения и освобождения, а не последовательно согласованных операций (что является порядком по умолчанию для операций над атомарным объектом, когда вы не указываете порядок памяти): все последовательно согласованные действия (операции над атомарный объект или забор) происходят в четко определенном глобальном порядке. Однако это ничего не значит для независимых потоков, не имеющих общих атомарных объектов.

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

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

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

person curiousguy    schedule 25.05.2019
comment
Получение мьютекса - это операция memory_order_acquire, согласно главе Atomics (цитируется в ответе @Alok Save), а освобождение - это операция освобождения (на самом объекте мьютекса). Этот ответ не поддерживает утверждение о том, что мьютексы можно удалить. Хотя это правдоподобно, потому что они не определены как acq_rel, поэтому получение может двигаться произвольно раньше, а выпуск произвольно поздно, что позволяет переупорядочивать что угодно с чем угодно. Ваш ответ должен привести этот аргумент или что-то подобное. Тогда у вас есть интересный момент. - person Peter Cordes; 18.11.2019
comment
@PeterCordes Я согласен, что они приобретают и выпускают операции; они не похожи на заборы (которые действуют на все), они похожи на атомарные нагрузки и хранилища (которые действуют на что-то: указанный атомарный объект). Аргумент тот же, что и для того, почему атомарные операции могут быть оптимизированы, когда объект не используется совместно между потоками. Я объясню это более ясно. - person curiousguy; 18.11.2019
comment
Вот что я сказал: отпустите операцию, а не забор. В противном случае они не могли бы изменить порядок, как я описал. - person Peter Cordes; 18.11.2019
comment
@PeterCordes Ограждение выпуска может многое изменить, поскольку неатомарные операции могут проходить ограждение в обоих направлениях, пока они не пересекают предыдущее атомарное хранилище. - person curiousguy; 19.11.2019
comment
@PeterCordes Этот ответ не подтверждает его утверждение о том, что мьютексы можно удалить Их можно скомпилировать как NOP, если они ничего не делают. Хотя это правдоподобно, поскольку они не определены как acq_rel Не имеет значения: эти мьютексы не взаимодействуют с другими потоками. поэтому получение может перемещаться произвольно раньше, а выпуск произвольно поздно, Не имеет значения: операции не перемещаются, а компилируются в ничто (подавляется в codegen). разрешить что-либо переупорядочивать с чем угодно Я не понимаю, как вы могли бы перемещать разблокировку вокруг блокировки. Я надеюсь, что вы не можете в целом. - person curiousguy; 19.11.2019
comment
Двигайтесь, пока он не наткнется на что-то еще, что он не может пройти мимо, очевидно. Так или иначе, заборы существуют в абстрактной машине C++. Вы можете удалить их только после того, как найдете порядок времени компиляции, соответствующий ассемблерному коду, который вы хотите создать. - person Peter Cordes; 19.11.2019
comment
@PeterCordes Тогда вы можете перемещать разблокировку только непосредственно перед следующей операцией блокировки, что в целом не похоже на хорошую идею и не приводит к исчезновению какой-либо из этих операций. Весь смысл моего (теперь более длинного) ответа в том, что они могут исчезнуть. - person curiousguy; 19.11.2019
comment
Давайте продолжим обсуждение в чате. - person curiousguy; 19.11.2019