Является ли неопределенным поведение запускать функцию-член в отдельном потоке параллельно с конструктором типа? - PullRequest
5 голосов
/ 16 марта 2020

Это сценарий, который вы никогда не должны делать, но https://timsong-cpp.github.io/cppwp/class.cdtor#4 заявляет:

Функции-члены, включая виртуальные функции ([class.virtual]), могут вызываться во время конструирования или уничтожения ([class.base.init]).

Имеет ли это место, если функции вызываются параллельно? То есть, игнорируя условие гонки, если A находится в середине конструкции и frobme вызывается в какой-то момент ПОСЛЕ вызова конструктора (например, во время построения), это все еще определенное поведение?

#include <thread>

struct A {
    void frobme() {}
};

int main() {
    char mem[sizeof(A)];

    auto t1 = std::thread([mem]() mutable { new(mem) A; });
    auto t2 = std::thread([mem]() mutable { reinterpret_cast<A*>(mem)->frobme(); });

    t1.join();
    t2.join();
}

В качестве отдельного сценария мне также было указано, что конструктор A может создать несколько потоков, где эти потоки могут вызывать функцию-член до того, как A станет законченное построение, но порядок этих операций был бы более анализируемым (вы знаете, что гонок не будет, пока ПОСЛЕ потока не будет сгенерировано в конструкторе).

Ответы [ 2 ]

4 голосов
/ 17 марта 2020

Здесь есть две проблемы: ваш указанный c код и ваш общий вопрос.

В вашем указанном c коде, даже в лучшем из возможных сценариев (где t2 выполняется после t1), у вас есть гонка данных из-за отсутствия синхронизации между созданием и использованием. И это делает ваш код UB независимо от порядка выполнения.

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

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

Если предположить, что ни один из этих случаев не подходит, тогда все должно быть хорошо (в основном. Можно определить A таким образом что ваш reinterpret_cast не возвращает пригодный для использования указатель на A, который вы создали в этом хранилище; вам потребуется launder it ). Доступ к строящемуся / разрушаемому объекту возможен, но только определенным образом. Придерживайтесь этих путей, и у вас все будет хорошо ... с одним возможным уловом.

В этом ничего нет Стандарт о гонках данных при завершении инициализации объекта, только при конфликтах в ячейках памяти. Как только объект полностью сконструирован, поведение virtual функций может измениться в зависимости от изменения указателей vtable и тому подобное, если тип dynamici c является классом, производным от класса, данного другому потоку. Я не верю, что в разделе об объектной модели есть четкое утверждение об этом.

Также обратите внимание, что C ++ 20 добавил специальное правило к class.cdtor :

Во время конструирования объекта, если к значению объекта или любому из его подобъектов обращаются через glvalue, который не получен, прямо или косвенно, из указателя this конструктора, значение полученный таким образом объект или подобъект не указан.

3 голосов
/ 16 марта 2020

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

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

См. [basic.life] для полного списка операций, которые разрешены и запрещены.

В частности, одно из ограничений заключается в том, что

Программа имеет неопределенное поведение, если:

...

  • glvalue используется для вызова функции-члена c объекта

, не относящейся к состоянию, которое явно запрещает ваш пример.

Также [class.cdtor] скажем s:

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

и даже если вы выполняете синхронизацию с каким-либо событием, инициируемым после начала строительства, это правило запрещает этот код:

Во время построения объекта, если значение объекта или любого из его подобъектов доступно через glvalue, который не получен, прямо или косвенно, из указателя this конструктора, значение полученного таким образом объекта или подобъекта не определено

...