Раздел: Документация / Стандарт языка С++ /
1.10. Многопоточные выполнения и гонка по данным
Все способы изучить С++
Начинающие программисты даже не догадываются о том, какой огромный пласт в этой области скрыт от их глаз, и чего многие из новичков не увидят никогда, потому что это тёмная сторона программирования - чистый исходный код системного уровня… Подробнее... |
1. Поток выполнения (также известный как поток - thread) - это единый поток управления в программе, включающий начальный вызов определенной функции верхнего уровня и рекурсивно включающий каждый вызов функции, впоследствии выполняемый потоком. [ Примечание: Когда один поток создает другой, первоначальный вызов функции верхнего уровня нового потока выполняется новым потоком, а не создающим потоком. — конец примечания ] Каждый поток в программе потенциально может получить доступ к каждому объекту и функции в программе (объект с автоматической или потоковой продолжительностью хранения (3.7) связан с одним конкретным потоком и может быть доступен другому потоку только косвенно через указатель или ссылку (3.9.2)). В конкретной реализации (hosted implementation) программа на C++ может иметь более одного потока, работающего одновременно. Выполнение каждого потока выполняется в соответствии с остальной частью этого стандарта. Выполнение всей программы состоит из выполнения всех ее потоков. [ Примечание: Обычно выполнение можно рассматривать как чередование всех его потоков. Однако некоторые виды атомарных операций, например, допускают выполнение, несовместимое с простым чередованием, как описано ниже. — конец примечания ] В автономной реализации определяется реализацией, может ли программа иметь более одного потока выполнения.
2. Обработчик сигнала, выполняемый в результате вызова функции raise
, принадлежит к тому же
потоку выполнения, что и вызов функции raise
. В противном случае неизвестно, какой поток выполнения содержит вызов обработчика сигнала.
3. Реализации должны гарантировать, что все разблокированные потоки в конечном итоге достигнут прогресса. [ Примечание: Стандартные библиотечные функции могут автоматически блокировать ввод-вывод или блокировки. Факторы в среде выполнения, в том числе внешние приоритеты потоков, могут помешать реализации обеспечить определенные гарантии дальнейшего прогресса. — конец примечания ]
4. Выполнение атомарных функций, которые либо определены как свободные от блокировки (29.7), либо указаны как свободные от блокировки (29.4), является выполнением без блокировки (lock-free execution).
(4.1) — Если есть только один разблокированный поток, в этом потоке должно завершиться выполнение без блокировки. [ Примечание: Одновременное выполнение потоков может препятствовать выполнению без блокировки. Например, такая ситуация может возникнуть в реализациях с блокировкой записи при условии блокировки загрузки. Это свойство иногда называют отсутствием препятствий (obstruction-free). — конец примечания ]
(4.2) — Когда одно или несколько выполнений без блокировки выполняются одновременно, по крайней мере одно должно быть завершено. [ Примечание: Некоторым реализациям трудно обеспечить абсолютные гарантии на этот счет, поскольку повторяющиеся и особенно несвоевременные помехи со стороны других потоков могут препятствовать дальнейшему прогрессу, например, путем многократного изъятия строки кэша для несвязанных целей между инструкциями с блокировкой загрузки и с условием сохранения. Реализации должны гарантировать, что такие эффекты не могут бесконечно задерживать прогресс в ожидаемых условиях эксплуатации, и поэтому программисты могут безопасно игнорировать такие аномалии. За пределами этого международного стандарта это свойство иногда называют незамкнутым. — конец примечания ]
ПРИМЕЧАНИЕ ПЕРЕВОДЧИКА: Прогресс - это ход выполнения программы, потока, процесса...
5. Значение объекта, видимого потоку T в определенной точке, является начальным значением объекта, значением, присвоенным объекту T, или значением, присвоенным объекту другим потоком, в соответствии с приведенными ниже правилами. [ Примечание: В некоторых случаях вместо этого может наблюдаться неопределенное поведение. Большая часть этого раздела мотивирована желанием поддерживать атомарные операции с явными и подробными ограничениями видимости. Однако он также неявно поддерживает более простое представление для программ с большими ограничениями. — конец примечания ]
6. Две оценки выражений конфликтуют, если одна из них изменяет ячейку памяти (1.7), а другая обращается к той же ячейке памяти или изменяет ее.
7. Библиотека определяет ряд атомарных операций (пункт 29) и операций над мьютексами (пункт 30), которые специально определены как операции синхронизации. Эти операции играют особую роль в том, чтобы задания в одном потоке были видны другому. Операция синхронизации в одной или нескольких ячейках памяти является либо операцией потребления, либо операцией потребления, либо операцией освобождения, либо как операцией потребления, так и операцией освобождения. Операция синхронизации без связанной ячейки памяти является забором (преградой) и может быть либо забором для потребления, либо забором для освобождения, либо как забором потребления, так и забором освобождения. Кроме того, существуют упрощенные атомарные операции, которые не являются операциями синхронизации, и атомарные операции чтения-изменения-записи, которые имеют особые характеристики. [ Примечание: Например, вызов, который получает мьютекс, выполнит операцию потребления в местоположениях, содержащих мьютекс. Соответственно, вызов, который освобождает один и тот же мьютекс, выполнит операцию освобождения в тех же местах. Неофициально выполнение операции высвобождения для A заставляет предыдущие побочные эффекты в других ячейках памяти становиться видимыми для других потоков, которые позже выполняют потребление или операцию потребления в A. “Расслабленные” атомарные операции не являются операциями синхронизации, хотя, как и операции синхронизации, они не могут способствовать гонке данных. — конец примечания ]
8. Все модификации конкретного атомарного объекта M происходят в некотором определенном общем порядке, называемом порядком модификации M. Если A и B являются модификациями атомарного объекта M, и A появляется перед (как определено ниже) B, то A должен предшествовать B в порядке модификации M, который определен ниже. [ Примечание: Здесь указано, что порядки модификации должны соответствовать соотношению “происходит до”. — конец примечания ] [ Примечание: Для каждого атомарного объекта существует отдельный порядок. Нет требования, чтобы они могли быть объединены в единый общий порядок для всех объектов. В общем случае это будет невозможно, так как разные потоки могут наблюдать изменения разных объектов в несогласованных порядках. — конец примечания ]
9. Последовательность освобождения, возглавляемая операцией освобождения A для атомарного объекта M, представляет собой максимальную непрерывную последовательность побочных эффектов в ходе модификации M, где первой операцией является A, и каждая последующая операция
(9.1) — выполняется тем же потоком, который выполнил A, или
(9.2) — это атомарная операция чтения-изменения-записи.
10. Некоторые вызовы библиотеки синхронизируются с другими вызовами библиотеки, выполняемыми другим потоком. Например, атомарное освобождение хранилища синхронизируется с получением загрузки, которое берет свое значение из хранилища (29.3). [ Примечание: За исключением указанных случаев, чтение более позднего значения не обязательно обеспечивает видимость, как описано ниже. Такое требование в некоторых случаях мешало бы эффективному выполнению. — конец примечания ] [ Примечание: Спецификации операций синхронизации определяют, когда одна считывает значение, записанное другой. Для атомарных объектов определение ясно. Все операции с данным мьютексом выполняются в одном общем порядке. Каждое получение мьютекса “считывает значение, записанное” последним освобождением мьютекса. — конец примечания ]
11. Оценка A имеет зависимость от оценки B, если
(11.1) — значение A используется в качестве операнда B, за исключением:
(11.1.1) — B - это вызов каких-либо специализаций в std::kill_dependency (29.3), или
(11.1.2) — это левый операнд встроенного логического И (&&, см. 5.14) или логического ИЛИ (||, см. оператор 5.15), или
(11.1.3) — это левый операнд условного оператора (?:, смотрите 5.16), или
(11.1.4) — это левый операнд встроенного оператора-разделителя (,) (5.19); или
(11.2) — A записывает скалярный объект или битовое поле M, а B считывает значение, записанное с помощью A из M, и A в последовательности перед B, или
(11.3) — для некоторой оценки X, A имеет зависимость от X, а X имеет зависимость от B.
[ Примечание: “Имеет зависимость от” - это подмножество “в последовательности перед” и также строго внутри потока. — конец примечания ]
12. Оценка A упорядочивается по зависимостям перед оценкой B, если
(12.1) — A выполняет операцию освобождения атомарного объекта M, а в другом потоке B выполняет операцию потребления над M и считывает значение, записанное любым побочным эффектом в последовательности освобождения, возглавляемой A, или
(12.2) — для некоторой оценки X, A упорядочен по зависимостям до X, а X переносит зависимость в B.
[ Примечание: Отношение “упорядочено по зависимостям до” аналогично “синхронизируется с”, но использует освобождение/-потребление вместо освобождения/приобретения. — конец примечания ]
13. Оценка A между потоками происходит до оценки B, если
(13.1) — A синхронизируется с B, или
(13.2) — A упорядочен по зависимостям перед B, или
(13.3) — для некоторой оценки X
(13.3.1) — A синхронизируется с X, и X упорядочивается перед B, или
(13.3.2) — A упорядочивается перед X и межпоточный X появляется перед B, или
(13.3.3) — межпоточный A появляется перед X и межпоточный X появляется перед B.
[ Примечание: Отношение “между потоками происходит до (inter-thread happens before)” описывает произвольные объединения отношений “упорядочено до (sequenced before)”, “синхронизируется с (synchronizes with)” и “упорядочено по зависимостям до (dependency-ordered before)”, за двумя исключениями. Первое исключение состоит в том, что объединение не разрешается заканчивать словами “упорядоченный по зависимостям до (dependency-ordered before)”, за которыми следует “упорядоченный до (sequenced before)”. Причина этого ограничения заключается в том, что операция потребления, участвующая в отношениях “порядок зависимостей до (dependency-ordered before)”, обеспечивает упорядочение только в отношении операций, к которым эта операция потребления фактически имеет зависимость. Причина, по которой это ограничение применяется только к концу такой конкатенации, заключается в том, что любая последующая операция освобождения обеспечит необходимый порядок для предыдущей операции потребления. Второе исключение заключается в том, что конкатенация не может полностью состоять из “упорядоченных до (sequenced before)”. Причины этого ограничения заключаются в том, (1) чтобы разрешить транзитивное закрытие “между потоками появляется до (inter-thread happens before)” и (2) отношение “происходит до (happens before)”, определенное ниже, предусматривает отношения, полностью состоящие из “упорядоченных до (sequenced before)”. — конец примечания ]
14. Оценка A происходит до оценки B, если:
(14.1) — A упорядочивается перед B, или
(14.2) — межпоточный A появляется перед B.
Реализация должна гарантировать, что ни одно выполнение программы не демонстрирует цикл в отношении “происходит раньше". [ Примечание: В противном случае этот цикл был бы возможен только с помощью операций потребления. — конец примечания ]
15. Видимый побочный эффект A для скалярного объекта или битового поля M относительно вычисления значения B из M удовлетворяет условиям:
(15.1) — A происходит до B и
(15.2) — нет никакого другого побочного эффекта от X до M, такого, что A происходит до X, а X происходит до B.
Значение не атомарного скалярного объекта или битового поля M, как определено при оценке B, должно быть значением, сохраненным видимым побочным эффектом A. [ Примечание: Если существует неопределенность в отношении того, какой побочный эффект для не атомарного объекта или битового поля виден, то поведение либо не указано, либо не определено. — конец примечания ] [ Примечание: Это указывает на то, что операции с обычными объектами не переупорядочиваются видимым образом. На самом деле это невозможно обнаружить без гонки данных, но необходимо убедиться, что гонки данных, как определено ниже, и с соответствующими ограничениями на использование атомарных элементов соответствуют гонкам данных при простом чередующемся (последовательно согласованном) выполнении. — конец примечания ]
16. Значение атомарного объекта M, определенное оценкой B, должно быть значением, сохраненным некоторым побочным эффектом A, который изменяет M, где B не происходит до A. [ Примечание: Набор таких побочных эффектов также ограничен остальными правилами, описанными здесь, и, в частности, в соответствии с приведенными ниже требованиями к соглашениям. — конец примечания ]
17. Если операция A, которая изменяет атомарный объект M, выполняется перед операцией B, которая изменяет M, то A должно быть раньше, чем B в порядке изменения M. [ Примечание: Это требование известно как соглашение запись-запись. — конец примечания ]
18. Если вычисление значения A атомарного объекта M происходит до вычисления значения B для M, и A берет свое значение из побочного эффекта X на M, то значение, вычисленное в B, должно быть либо значением, сохраненным X, либо значением, сохраненным побочным эффектом Y на M, где Y следует за X в порядке изменения M. [ Примечание: Это требование известно как соглашение чтение-чтение. — конец примечания ]
19. Если вычисление значения A атомарного объекта M происходит перед операцией B, которая изменяет M, то A должно принимать свое значение из побочного эффекта X на M, где X предшествует B в порядке изменения M. [ Примечание: Это требование известно как соглашение чтение-запись. — конец примечания ]
20. Если побочный эффект X для атомарного объекта M происходит до вычисления значения B из M, то оценка B должна принимать свое значение из X или из побочного эффекта Y, который следует за X в порядке изменения M. [ Примечание: Это требование известно как соглашение запись-чтение. — конец примечания ]
21. [ Примечание: Четыре предыдущих требования к соглашениям эффективно запрещают компилятору переупорядочивать атомарные операции для одного объекта, даже если обе операции являются расслабенными загрузками. Это эффективно гарантирует согласованность кэша, предоставляемую большинством аппаратных средств, доступных для атомарных операций C++. — конец примечания ]
22. [ Примечание: Значение, наблюдаемое при загрузке атома, зависит от отношения “происходит раньше”, которое зависит от значений, наблюдаемых при загрузке атомов. Предполагаемое чтение состоит в том, что должна существовать связь атомарных загрузок с изменениями, которые они наблюдают, что вместе с соответствующим образом выбранными порядками модификации и отношением “происходит раньше”, полученным, как описано выше, удовлетворяет результирующим ограничениям, наложенным здесь. — конец примечания ]
23. Два действия потенциально являются одновременными, если
(23.1) — они выполняются разными потоками, или
(23.2) — они не упорядочены, и по крайней мере один выполняется обработчиком сигналов.
Выполнение программы содержит гонку данных, если она содержит два потенциально одновременных
конфликтующих действия, по крайней мере, одно из которых не является атомарным, и ни одно из
них не происходит раньше другого, за исключением особого случая для обработчиков сигналов,
описанного ниже. Любая такая гонка данных приводит к неопределенному поведению. [ Примечание:
Можно показать, что программы, которые правильно используют мьютексы и операции memory_order_seq_cst
для предотвращения всех гонок данных и не используют никаких других операций синхронизации,
ведут себя так, как если бы операции, выполняемые составляющими их потоками, просто чередовались,
при этом каждое вычисление значения объекта берется из последнего побочного эффекта для этого объекта
в этом чередовании. Обычно это называется “последовательной согласованностью”.
Однако это относится только к программам без гонки данных, а программы без гонки данных не могут
наблюдать большинство программных преобразований, которые
не изменяют семантику однопоточной программы. Фактически, большинство однопоточных программных преобразований
по-прежнему разрешены, поскольку любая программа, которая в результате ведет себя по-другому,
должна выполнять неопределенную операцию. — конец примечания ]
24. Два обращения к одному и тому же объекту типа volatile sig_atomic_t
не приводят к гонке данных,
если оба происходят в одном и том же потоке, даже если одно или несколько обращений происходит в
обработчике сигналов. Для каждого вызова обработчика сигнала оценки, выполняемого потоком,
вызывающим обработчик сигнала, могут быть разделены на две группы A и B, так что никакие оценки в B не
выполняются до оценок в A, а оценки таких объектов как volatile sig_atomic_t
принимают значения, как если бы все оценки в A выполнялись до выполнения обработчика сигнала и выполнение обработчика сигнала произошли до всех вычислений в B.
25. [ Примечание: Преобразования компилятора, которые вводят назначения в потенциально разделяемую ячейку памяти, которая не будет изменена абстрактной машиной, обычно запрещены этим стандартом, поскольку такое назначение может перезаписать другое назначение другим потоком в случаях, когда выполнение абстрактной машины не столкнулось бы с гонкой данных. Это включает в себя реализации назначения элементов данных, которые перезаписывают соседние элементы в отдельных ячейках памяти. Изменение порядка атомарных нагрузок в случаях, когда рассматриваемые атомарные элементы могут быть псевдонимами, также обычно исключается, поскольку это может нарушить правила согласованности. — конец примечания ]
26. [ Примечание: Преобразования, которые вводят спекулятивное чтение потенциально разделяемой ячейки памяти, могут не сохранять семантику программы C++, как определено в этом стандарте, поскольку они потенциально приводят к гонке данных. Однако они обычно действительны в контексте оптимизирующего компилятора, который нацелен на конкретную машину с четко определенной семантикой для гонок данных. Они были бы недействительны для гипотетической машины, которая не терпима к гонкам или обеспечивает обнаружение аппаратных гонок. — конец примечания ]
27. Реализация может предполагать, что любой поток в конечном итоге выполнит одно из следующих действий:
(27.1) — завершение,
(27.2) — вызов библиотечной функции ввода-вывода,
(27.3) — доступ или изменение изменяемого объекта, или
(27.4) — выполнит операцию синхронизации или атомарную операцию.
[ Примечание: Это предназначено для того, чтобы разрешить преобразования компилятора, такие как удаление пустых циклов, даже если завершение не может быть доказано. — конец примечания ]
28. Реализация должна гарантировать, что последнее значение (в порядке изменения), присвоенное атомарной операцией или операцией синхронизации, станет видимым для всех других потоков за конечный период времени.
Вступить в группу "Основы программирования"
Подписаться на канал в RUTUBE Подписаться на Дзен-канал Подписаться на рассылки по программированию |
Как стать программистом 2.0
Эта книга для тех, кто хочет стать программистом. На самом деле хочет, а не просто мечтает. И хочет именно стать программистом с большой буквы, а не просто научиться кулебякать какие-то примитивные программки… Подробнее... |