С++, используя этот указатель в конструкторах

В C++ во время конструктора класса я начал новый поток с указателем this в качестве параметра, который будет широко использоваться в потоке (например, вызывает функции-члены). Это плохо? Почему и каковы последствия?

Мой процесс запуска начинается в конце конструктора.

Ответ 1

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

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

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

Ответ 2

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

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

Пример:

struct BaseThread {
    MyThread() {
        pthread_create(thread, attr, pthread_fn, static_cast<void*>(this));
    }
    virtual ~MyThread() {
        maybe stop thread somehow, reap it;
    }
    virtual void id() { std::cout << "base\n"; }
};

struct DerivedThread : BaseThread {
    virtual void id() { std::cout << "derived\n"; }
};

void* thread_fn(void* input) {
    (static_cast<BaseThread*>(input))->id();
    return 0;
}

Теперь, если вы создаете DerivedThread, это лучшая гонка между потоком, который ее создает, и новым потоком, чтобы определить, какая версия id() вызывается. Может случиться так, что может произойти что-то еще хуже, вам нужно будет внимательно посмотреть на свой API-интерфейс и компилятор потоков.

Обычный способ не беспокоиться об этом - просто дать вашему потоку класс функцию start(), которую пользователь вызывает после ее создания.

Ответ 3

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

Вы можете уменьшить риски с помощью метода factory, который сначала создает объект, а затем запускает поток.

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

Ответ 4

Это может быть потенциально опасно.

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

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

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

Ответ 5

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

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

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

Ответ 6

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

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

Таким образом, самым безопасным может быть, вероятно, вручную запустить поток:

class Thread { 
  public: 
    Thread();
    virtual ~Thread();
    void start();
    // ...
};

class MyThread : public Thread { 
  public:
    MyThread() : Thread() {}
    // ... 
};

void f()
{
  MyThread thrd;
  thrd.start();
  // ...
}

Ответ 7

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

Ответ 8

Некоторые люди считают, что вы не должны использовать указатель this в конструкторе, потому что объект еще не сформирован полностью. Однако вы можете использовать это в конструкторе (в {body} и даже в списке инициализации), если вы будете осторожны.

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

Вот что-то, что никогда не работает: {body} конструктора (или функции, вызванной конструктором) не может перейти к производному классу, вызвав функцию virtualmember, которая переопределена в производном классе. Если ваша цель состояла в том, чтобы перейти к переопределенной функции в производном классе, вы не получите то, что хотите. Обратите внимание, что вы не получите переопределение в производном классе независимо от того, как вы вызываете функцию виртуального участника: явно используя этот указатель (например, this- > method()), неявно используя этот указатель (например, метод ( )) или даже вызов какой-либо другой функции, которая вызывает функцию виртуального члена на вашем этом объекте. Суть в том, что даже если вызывающий объект создает объект производного класса, во время конструктора базового класса ваш объект еще не является этим производным классом. Вы были предупреждены.

Вот что-то, что иногда срабатывает: если вы передаете какой-либо элемент данных этого объекта другому инициализатору элемента данных, вы должны убедиться, что другой элемент данных уже инициализирован. Хорошей новостью является то, что вы можете определить, был ли другой элемент данных (или не был) инициализирован с использованием некоторых простых правил языка, которые не зависят от конкретного используемого вами компилятора. Плохая новость заключается в том, что вы должны знать эти языковые правила (например, сначала инициализируются под-объекты базового класса (посмотрите порядок, если у вас есть множественное и/или виртуальное наследование!), Тогда члены данных, определенные в классе, инициализируются в порядок, в котором они появляются в объявлении класса). Если вы не знаете эти правила, не передавайте ни одного члена данных из этого объекта (независимо от того, используете ли вы это явное использование), любому другому инициализатору элемента данных! И если вы знаете правила, будьте осторожны.