2010-03-25 5 views
25

In C++, durante un costruttore di classi, ho iniziato una nuova discussione con il puntatore this come parametro che verrà utilizzato nel thread in modo esteso (ad esempio, chiamando le funzioni membro). È una brutta cosa da fare? Perché e quali sono le conseguenze?C++ utilizzando questo puntatore nei costruttori

Il mio processo di avvio del thread si trova alla fine del costruttore.

risposta

18

La conseguenza è che il thread può essere avviato e il codice inizierà l'esecuzione di un oggetto non ancora completamente inizializzato. Il che è già abbastanza grave.

Se stai considerando che 'beh, sarà l'ultima frase nel costruttore, sarà quasi costruito come si ottiene ...' ripensaci: potresti derivare da quella classe, e l'oggetto derivato non sarà costruito

Il compilatore può decidere di giocare con il vostro codice intorno e decidere che sarà riordinare le istruzioni e potrebbe realmente passare il puntatore this prima di eseguire qualsiasi altra parte del codice multithreading ... è difficile

+1

@gilbertc: se B deriva da A, anche se si avvia il thread alla fine del costruttore di A, tutto da B (vtable, variabili membro, codice costruttore) non verrà inizializzato/eseguito. Potrebbe essere un brutto bug di concorrenza? A prima vista, direi di sì, perché l'oggetto cambierà nel thread di costruzione, mentre potrebbe essere utilizzato nel thread appena creato. – paercebal

1

dipende da ciò che fai dopo aver avviato il thread. Se si eseguono i lavori di inizializzazione dopo, il thread è stato avviato, quindi potrebbe utilizzare dati non inizializzati correttamente.

È possibile ridurre i rischi utilizzando un metodo factory che prima crea un oggetto, quindi avvia il thread.

Ma penso che il più grande difetto del design sia che, almeno per me, un costruttore che fa più della "costruzione" sembra abbastanza confuso.

0

Va bene, a patto che sia possibile iniziare a utilizzare quel puntatore subito. Se è necessario il resto del costruttore per completare l'inizializzazione prima che il nuovo thread possa utilizzare il puntatore, è necessario eseguire una sincronizzazione.

4

La conseguenza principale è che il thread potrebbe iniziare a funzionare (e usare il puntatore) prima che il costruttore sia completato, quindi l'oggetto potrebbe non essere in uno stato definito/utilizzabile. Allo stesso modo, a seconda di come il thread viene interrotto, potrebbe continuare a essere eseguito dopo l'avvio del distruttore e quindi l'oggetto potrebbe non essere in uno stato utilizzabile.

Questo è particolarmente problematico se la classe è una classe base, poiché il costruttore della classe derivata non inizierà nemmeno l'esecuzione fino a quando non sarà terminato il costruttore e il distruttore della classe derivata avrà completato prima dell'avvio del proprio. Inoltre, le chiamate alle funzioni virtuali non fanno ciò che si potrebbe pensare prima che le classi derivate siano costruite e dopo che sono state distrutte: le chiamate virtuali "ignorano" le classi la cui parte dell'oggetto non esiste.

Esempio:

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; 
} 

Ora, se si crea un DerivedThread, si tratta di una migliore una gara tra il filo che costruisce e il nuovo thread, per determinare quale versione di id() viene chiamato. Potrebbe succedere che qualcosa di peggio possa succedere, avresti bisogno di guardare abbastanza da vicino la tua API e compilatore di threading.

Il modo usuale di non doversi preoccupare di questo è solo per dare alla classe thread una funzione start(), che l'utente chiama dopo averla costruita.

1

Può essere potenzialmente pericoloso.

Durante la costruzione di una classe di base, tutte le chiamate a funzioni virtuali non verranno annullate per eseguire l'override in più classi derivate che non sono state ancora completamente costruite; una volta che la costruzione delle classi più derivate modifica questi cambiamenti.

Se il thread che kick-off chiama una funzione virtuale ed è indeterminato dove ciò accade in relazione al completamento della costruzione della classe, è probabile che si verifichi un comportamento imprevedibile; forse un incidente.

Senza funzioni virtuali, se il thread utilizza solo metodi e dati delle parti della classe che sono state costruite completamente, è probabile che il comportamento sia prevedibile.

1

Direi che, come regola generale, dovresti evitare di farlo. Ma puoi sicuramente farla franca in molte circostanze. Penso che ci siano fondamentalmente due cose che possono andare storte:

  1. Il nuovo thread potrebbe tentare di accedere all'oggetto prima che il costruttore termini l'inizializzazione. È possibile aggirare questo assicurandosi che tutte le inizializzazioni siano complete prima di iniziare il thread. Ma cosa succede se qualcuno eredita dalla tua classe? Non hai alcun controllo su ciò che farà il loro costruttore.
  2. Cosa succede se il thread non viene avviato? Non esiste un modo veramente pulito per gestire gli errori in un costruttore. Puoi lanciare un'eccezione, ma questo è pericoloso poiché significa che il distruttore dell'oggetto non verrà chiamato. Se decidi di non lanciare un'eccezione, sei bloccato a scrivere codice nei vari metodi per verificare se le cose sono state inizializzate correttamente.

In generale, se si dispone di un'inizializzazione complessa e soggetta a errori, è meglio farlo in un metodo piuttosto che nel costruttore.

+0

Il mancato intervento del responsabile della costruzione non è pericoloso solo se la classe gestisce direttamente le risorse non elaborate, cosa che non dovrebbe comunque essere eseguita. Dal momento che le eccezioni sono l'unico modo per segnalare i fallimenti di costruzione, di certo non dovrebbero essere vietati al fine di permettere alle classi scarsamente implementate di non perdere. Sono d'accordo con tutto ciò che dici, però. – sbi

+0

Non ho detto che dovrebbe essere vietato, solo che è pericoloso. Ci vuole un certo grado di cura per assicurarsi che tutto sia pulito correttamente a fronte di un'eccezione. Questo può essere particolarmente difficile quando la tua classe dipende dal codice che non hai scritto e non controlli. –

1

In sostanza, quello che ti serve è due fasi di costruzione: Si desidera iniziare la discussione solo dopo l'oggetto è completamente costruito. John Dibling answered una domanda simile (non un duplicato) ieri che discute esaurientemente la costruzione in due fasi. Potresti volerlo dare un'occhiata.

Nota, tuttavia, questo lascia ancora il problema che il thread potrebbe essere avviato prima che venga eseguito il costruttore di una classe derivata. (. Costruttori classi derivate sono chiamati dopo quelli dei loro classi base)

Così, alla fine la cosa più sicura è probabilmente quello di avviare manualmente il filo:

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

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

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

Alcune persone si sentono non si dovrebbe usare il this puntatore in un costruttore perché l'oggetto non è ancora completamente formato. Comunque puoi usare questo nel costruttore (nel {body} e anche nella lista di inizializzazione) se stai attento.

Ecco qualcosa che funziona sempre: lo {body} di un costruttore (o una funzione chiamata dal costruttore) può accedere in modo affidabile ai membri dati dichiarati in una classe base e/o ai membri dati dichiarati nella classe del costruttore stesso. Questo perché tutti i membri di questi dati sono garantiti per essere completamente costruiti al momento in cui il {body} del costruttore inizia ad essere eseguito.

Ecco qualcosa che non funziona mai: il {body} di un costruttore (o una funzione chiamata dal costruttore) non può scendere in una classe derivata chiamando una funzione virtualmember che viene sovrascritta nella classe derivata.Se il tuo obiettivo era raggiungere la funzione sovrascritta nella classe derivata, non otterrai ciò che desideri. Si noti che non si otterrà l'override nella classe derivata indipendentemente da come si chiama la funzione membro virtuale: utilizzando esplicitamente questo puntatore (ad esempio, this-> method()), utilizzando implicitamente questo puntatore (ad esempio, metodo ()), o anche chiamando qualche altra funzione che chiama la funzione membro virtuale su questo oggetto. La linea di fondo è questa: anche se il chiamante sta costruendo un oggetto di una classe derivata, durante il costruttore della classe base, il tuo oggetto non è ancora di quella classe derivata. Sei stato avvertito.

Ecco qualcosa che a volte funziona: se si passa uno qualsiasi dei membri dati in questo oggetto a un altro inizializzatore di un membro dati, è necessario assicurarsi che l'altro membro dati sia già stato inizializzato. La buona notizia è che puoi determinare se l'altro membro dei dati è stato (o non è) stato inizializzato usando alcune regole linguistiche semplici che sono indipendenti dal particolare compilatore che stai utilizzando. La cattiva notizia è che devi conoscere queste regole linguistiche (ad esempio, gli oggetti secondari della classe base vengono inizializzati per primi (controlla l'ordine se hai ereditarietà multipla e/o virtuale!), Quindi i membri dati definiti nella classe vengono inizializzati in l'ordine in cui compaiono nella dichiarazione della classe). Se non si conoscono queste regole, non passare alcun membro di dati da questo oggetto (indipendentemente dal fatto che si utilizzi esplicitamente la thiskeyword) a qualsiasi inizializzatore di un altro membro di dati! E se conosci le regole, per favore fai attenzione.