Является ли вызов метода объекта во время вызова уничтожения из другого потока неопределенным поведением?

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

проект стандарта C++ 14 (12.7.4) говорит:

Member functions, including virtual functions (10.3), can be called during construction or destruction (12.6.2). When a virtual function is called directly or indirectly from a constructor or from a destructor, including during the construction or destruction of the class’s non-static data members, and the object to which the call applies is the object (call it x) under construction or destruction, the function called is the final overrider in the constructor’s or destructor’s class and not one overriding it in a more-derived class.

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

Соответствующий пример кода:

#include <vector>
#include <thread>
#include <atomic>
#include <iostream>

struct A {
  void reg() {
    thread_ = std::thread([this]() {
      while (a_ < 10) {
        pr();
      }
    });
  }

  void pr() {
    std::unique_lock<std::mutex> lock(mt_);
    std::cout << "Hello World\n";
    a_++;
  }

  ~A() {
    std::unique_lock<std::mutex> lock(mt_);
    std::cout << "Destruction started\n";
    lock.unlock();

    thread_.join();
  }

  int a_{0};
  std::mutex mt_;
  std::thread thread_;
};

int main() {
  A a;
  a.reg();
}

PS: Я знаю, что мне нужно синхронизировать доступ к полям и быть осторожным с остановкой обратных вызовов после выхода из тела деструктора.

PPS: То же самое и с виртуальными методами? Возможно ли, чтобы виртуальный вызов был отправлен для переопределения метода в производном классе (какие данные уже были уничтожены)? Согласно приведенной выше цитате, этого не должно быть. Но я все еще не уверен, можно ли применить его к многопоточным сценариям.

Извините, вы можете показать пример в реальном коде? У меня проблемы с пониманием.

Brian Bi 13.09.2018 18:14

Это в значительной степени определение гонка данных. Да, поведение не определено.

Pete Becker 13.09.2018 18:15

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

NathanOliver 13.09.2018 18:29

@PeteBecker Я не думаю, что это определение гонки за данные. Добавлен пример.

Sukhanov Niсkolay 13.09.2018 19:08

Похоже, что ответ отрицательный, он четко определен, потому что: Member functions, including virtual functions (10.3), can be called during construction or destruction (12.6.2).

Sukhanov Niсkolay 13.09.2018 19:09

<shrug> - ваш недавно добавленный пример намного уже, чем ваш вопрос. Если ваш деструктор и ваш обратный вызов не делают ничего, что вызывает гонку данных, тогда, да, это нормально; в противном случае нет. Но это бесполезно, потому что у вас нет возможности узнать, правда ли это. Цитируемый вами текст [class.cdtor] / 4 не относится к многопоточным приложениям. Для этого вам нужно посмотреть [intro.multithread].

Pete Becker 13.09.2018 19:32

«Но это бесполезно, потому что у вас нет возможности узнать, правда ли это». почему это так? Я могу положиться на реализацию деструктора, т.е. так же, как в примере.

Sukhanov Niсkolay 13.09.2018 19:46

@PeteBecker еще один вопрос, если можно, «тогда да, все в порядке», почему это так? Я понимаю ваши рассуждения о том, что стандарт ничего не определяет в параграфе 12.7.4 о многопоточности, и отсюда мы можем сделать вывод, что он действительно не определен. Нам нужен другой абзац, чтобы сказать, что это нормально. Кроме того, возникает вопрос, будет ли виртуальный вызов отправлен правильно (как предлагает 12.7.4)? Или, может быть, эти поиски могут привести к гонке данных, т.е. на vtable)? можем ли мы гарантировать, что вызов не будет отправлен на переопределения в производных классах (поля которых уже уничтожены)? как насчет невиртуального?

Sukhanov Niсkolay 14.09.2018 12:39

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

Pete Becker 14.09.2018 13:41

«есть сгенерированные компилятором доступы для уничтожения объектов» - они будут вызываться после выхода из деструктора, и это гарантируется стандартным imo. Я не планирую оставлять деструктор до тех пор, пока все остальные подписчики не будут отписаны, что гарантирует, что никакие дальнейшие вызовы не произойдут после выхода из тела деструктора или какого-либо примитива синхронизации в теле, которое, вероятно, заблокирует деструктор, пока я не очищу каждую ссылку на объект. Вопрос в том, правильно ли определен сам вызов после входа в деструктор? И будет ли этот вызов отправлен правильно, как в однопоточном сценарии?

Sukhanov Niсkolay 14.09.2018 16:18

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

Sukhanov Niсkolay 14.09.2018 16:23
0
11
202
1

Ответы 1

Да, в большинстве случаев это неопределенное поведение и очень небезопасно.

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

Однако это все равно будет небезопасно, и вы должны как можно больше стараться, чтобы этого не произошло.

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

Pete Becker 13.09.2018 18:18

@PeteBecker Да, поэтому я привел к ответу: «В большинстве случаев это не определено и небезопасно». Я считаю, что есть смысл признать возможные исключения из этого правила.

Xirema 13.09.2018 18:20

Другие вопросы по теме