2015-11-06 4 views
3

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

У меня есть что-то вроде этого:

class MyClass 
{ 
MyClass() : mExit(false), mThread(&MyClass::ThreadMain,this) 
{} 

void ThreadMain() 
{ 
    unique_lock<mutex> lock(mMutex); 
    mCondition.wait(lock, [&] { return mExit; }); 
} 

std::thread mThread; 
std::mutex mMutex; 
std::condition_variable mCondition; 
bool mExit; 
}; 

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

Похоже на некоторые примеры, которые я видел в Интернете, но я не уверен, что это определенно безопасно или нет.

ответ

7

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

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

+0

Я не осознавал инициализацию 'mutex' и' condition_variable' _needed_? Или вы говорите, что я должен сделать это явно, например. : 'MyClass(): mExit (false), mMutex(), mCondition(), mThread (& MyClass :: ThreadMain, this)' –

+1

@ Mr.Boy Я бы никогда не использовал неинициализированную переменную, поэтому да, я бы по умолчанию инициализировал их в конструктор. – NathanOliver

+0

@NathanOliver Не включая _objects_ в списке инициализации приведет к тому, что они будут _default initialized_. Включая их в список инициализаторов, передавая нулевые аргументы конструктору, объекты будут _value initialized_. – Snps

1

Да, это может иметь плохое поведение, поскольку mThread может быть запущен, пока экземпляр MyClass еще не построен.

Мое правило для большого пальца: если мне нужно использовать this в конструкторе, я делаю что-то непослушное;).

2

В дополнение к проблеме с вопросом-началом-заказом-нить-ранним исполнением, описанным @NathanOliver, я хотел бы указать, что код будет по-прежнему демонстрировать неопределенное поведение при использовании виртуального fuction вместо ThreadMain ,

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

Общее решение этой проблемы с обработчиком потоков RAII состоит в том, чтобы отделить инициализацию объекта от выполнения потока, , например., используя функцию start. Это также устранит зависимость от порядка построения членов.

Это гарантирует, что при запуске нитки будет построено MyClass. Теперь также можно использовать полиморфизм.

struct Derived : public MyClass { 
    virtual void ThreadMain() { 
     std::unique_lock<std::mutex> lock(mMutex); 
     mCondition.wait(lock, [&] { return mExit.load(); }); 
    } 
}; 

Тем не менее, в настоящее время поток должен быть запущен с помощью двух заявлений вместо одного, например, , MyClass m; m.start();. Чтобы обойти это, мы можем просто создать класс-оболочку, который выполняет функцию start в теле конструктора.

struct ThreadHandler { 
    ThreadHandler() { d.start(); } 
    Derived d; 
}; 
Смежные вопросы