2010-03-25 2 views
25

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

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

ответ

18

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

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

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

+1

@gilbertc: Если B происходит от A, даже если вы запустите поток в конце конструктора A, все из B (vtable, переменные-члены, код конструктора) не будут инициализированы/выполнены. Может ли это быть неприятной ошибкой параллелизма? На первый взгляд, я бы сказал, да, потому что объект будет меняться в потоке строительства, в то время как он может быть использован во вновь созданном потоке. – paercebal

1

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

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

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

0

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

4

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

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

Пример:

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

1

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

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

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

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

1

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

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

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

+0

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

+0

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

1

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

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

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

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

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

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

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

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

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

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

Смежные вопросы