C++ с помощью этого указателя в конструкторах

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

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

25
задан Kiril Kirov 16 November 2012 в 17:03
поделиться

7 ответов

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

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

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

]
19
ответ дан 28 November 2019 в 21:45
поделиться

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

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

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

1
ответ дан 28 November 2019 в 21:45
поделиться

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

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

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

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

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

void f()
{
  MyThread thrd;
  thrd.start();
  // ...
}
1
ответ дан 28 November 2019 в 21:45
поделиться

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

0
ответ дан 28 November 2019 в 21:45
поделиться

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

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

Пример:

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 () , которую пользователь вызывает после ее создания.

4
ответ дан 28 November 2019 в 21:45
поделиться

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

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

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

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

1
ответ дан 28 November 2019 в 21:45
поделиться

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

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

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

1
ответ дан 28 November 2019 в 21:45
поделиться
Другие вопросы по тегам:

Похожие вопросы: