Является ли гонка данных на vptr явно незаконной? - PullRequest
0 голосов
/ 30 июня 2018

Прежде чем идти дальше, обратите внимание: Это чисто языковой вопрос юриста . Я хочу получить ответы на основе стандартных цитат. Я не ищу совета по написанию кода C ++. Пожалуйста, ответьте, как если бы я был автором компилятора .

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

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

Во время уничтожения тип уменьшается (до конца тела деструктора этого подобъекта, где подобъект исчезает и больше не имеет динамического типа).

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

Реальный вопрос: Что происходит, если динамический тип объекта увеличивается в другом потоке?

Заголовок вопроса, который является стандартным C ++ вопросом , выражается с использованием нестандартного термина (vptr), что может показаться противоречивым. Причины:

  • Нет требования, чтобы полиморфизм был реализован в терминах vptr, но это (почти?) Всегда так. Один (или несколько) vptr в объекте представляет динамический тип полиморфного объекта.
  • Гонки данных определяются в терминах операций чтения / записи в ячейку памяти.
  • Стандартный текст часто использует нестандартные элементы «только для экспозиции», чтобы определить стандартные функции. (Так почему бы не использовать vptr «только для экспозиции»?)

Стандарт не определяет поведение полиморфных объектов (*) напрямую как функцию их динамического типа; стандарт определяет, какие выражения разрешены в течение так называемого «времени жизни» (после завершения конструктора), внутри тела конструктора самого производного типа (точно такие же выражения допускаются с той же семантикой), также внутри конструкторы подобъектов базового класса ...

(*) Динамическое поведение полиморфных или динамических объектов (**) включает в себя: виртуальные вызовы, производные от базовых преобразований, приведения вниз (static_cast или dynamic_cast), typeid полиморфного объекта.

(**) Динамический объект - это такой объект, в котором его класс использует ключевое слово virtual; по этой причине его конструктор не тривиален.

Итак, в описании говорится: После что-то закончилось, как только что-то началось, до что-то еще и т. Д. Какое-то выражение допустимо и делает то-то и то-то.

Спецификация конструкции и уничтожения была написана до того, как потоки стали частью стандарта C ++. Так что же изменилось со стандартизацией потоков? Есть одно предложение с определением поведения потоков (нормативная часть) [basic.life] / 11 :

В этом подпункте «до» и «после» относятся к «случается до» отношение ([intro.multithread]).

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

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

[Условие гонки - это случай недетерминированности, и любое значимое использование мьютекса, переменной условия, rwlocks, многократного использования семафоров, многократного использования других устройств синхронизации и любого использования атомарных примитивов вводит условие гонки. по крайней мере, на уровне порядка модификации на атомном объекте. Результатом того, что недетерминированность низкого уровня приводит к непредсказуемому высокоуровневому поведению, зависит способ использования примитивов.]

Затем стандартный черновик говорит:

[Примечание: следовательно, неопределенное поведение приводит к тому, что объект, который является на создание в одном потоке ссылаются из другого потока без адекватной синхронизации. - конец примечания]

Где определяется «адекватная синхронизация»?

Является ли отсутствие «адекватной синхронизации» моральным эквивалентом обычной гонки данных: гонки данных на vptr или, по общему мнению, гонки данных на динамическом типе?

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

Это вопрос языкового адвоката , поэтому я не заинтересован в:

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

РЕДАКТИРОВАТЬ: Предыдущий пример, вместо иллюстрации проблемы, отвлекал. Это вызвало очень интересную, но совершенно неуместную дискуссию в разделе чата.

Вот более чистый пример, который не вызовет той же проблемы:

atomic<Base1*> shared;

struct Base1 {
  virtual void f() {}
};

struct Base2 : Base1 {
  virtual void f() {}
  Base2 () { shared = (Base1*)this; }
};

struct Der2 : Base2 {
  virtual void f() {}
};

void use_shared() {
  Base1 *p;
  while (! (p = shared.get()));
  p->f();
}

С логикой потребителя / производителя:

  • Тема A: new Der2;
  • Резьба B: use_shared();

Для справки, оригинальный пример:

atomic<Base*> shared;

struct Base {
  virtual void f() {}
  Base () { shared = this; }
};

struct Der : Base {
  virtual void f() {}
};

void use_shared() {
  Base *p;
  while (! (p = shared.get()));
  p->f();
}

Логика потребителя / производителя:

  • Тема A: new Der;
  • Резьба B: use_shared();

Неясно, что this может использоваться другим потоком во время выполнения конструктора Base, что является интересной проблемой, но не имеет отношения к проблеме использования подобъекта базового класса, в то время как производный конструктор работает в другом нить.

Дополнительная информация

Для справки, DR, который "мотивировал" текущую формулировку (хотя это ничего не объясняет):

Базовый отчет о дефектах языка # 710

1 Ответ

0 голосов
/ 01 июля 2018

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

[basic.life] / 1 Время жизни объекта типа T начинается, когда ... его инициализация завершена.

Когда выполняется shared = this;, время жизни объекта Base, не говоря уже о Der, еще не началось.

[basic.life] / 6 До начала жизни объекта, но после того, как было выделено хранилище, которое будет занимать объект ... любой указатель, который представляет адрес места хранения, где объект может быть или был найден, может быть использован, но только ограниченным образом. О строящемся или разрушаемом объекте см. [class.cdtor] . В противном случае ... [t] программа имеет неопределенное поведение, если ... указатель используется для доступа к нестатическому члену данных или вызова нестатической функции-члена объекта.

[basic.life] / 11 В этом разделе «до» и «после» относятся к соотношению «происходит до» (4.7). [ Примечание: Поэтому неопределенное поведение возникает, если на объект, который создается в одном потоке, ссылаются из другого потока без адекватная синхронизация. - конец примечания ]

Таким образом, позиция по умолчанию [basic.life] заключается в том, что вызов метода объекта, который не происходит - после завершения его инициализации, имеет неопределенное поведение. Но [class.cdtor] может сказать больше.

[class.cdtor] / 3 Функции-члены, включая виртуальные функции (13.3), могут вызываться во время создания или уничтожения (15.6.2). Когда виртуальная функция вызывается прямо или косвенно из конструктора или из деструктора ...

Таким образом, [class.cdtor] относится только к случаю, когда виртуальная функция вызывается прямо или косвенно из конструктора (обязательно в том же потоке, в котором работает сам конструктор). В нем ничего не сказано о том, что метод вызывается из другого потока, как в примере. Я понимаю, что [basic.life] контролирует, и поведение примера не определено.

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