Cout è sincronizzato / thread-safe?


112

In generale presumo che i flussi non siano sincronizzati, spetta all'utente eseguire il blocco appropriato. Tuttavia, cose come coutottenere un trattamento speciale nella libreria standard?

Cioè, se più thread stanno scrivendo, coutpossono danneggiare l' coutoggetto? Capisco che anche se sincronizzato avresti comunque un output interlacciato casualmente, ma è quell'interlacciamento garantito. Cioè, è sicuro da usare coutda più thread?

Questo fornitore dipende? Cosa fa gcc?


Importante : fornisci un qualche tipo di riferimento per la tua risposta se dici "sì" poiché ho bisogno di una sorta di prova di ciò.

La mia preoccupazione non riguarda anche le chiamate di sistema sottostanti, quelle vanno bene, ma i flussi aggiungono uno strato di buffering in cima.


2
Questo dipende dal fornitore. C ++ (prima di C ++ 0x) non ha la nozione di thread multipli.
Sven

2
Che mi dici di c ++ 0x? Definisce un modello di memoria e cos'è un thread, quindi forse queste cose sono gocciolate nell'output?
rubenvb

2
Esistono fornitori che lo rendono thread-safe?
edA-qa mort-ora-y

Qualcuno ha un collegamento al più recente standard proposto da C ++ 2011?
edA-qa mort-ora-y

4
In un certo senso è qui che printfbrilla quando l'intero output viene scritto stdoutin un colpo solo; quando si utilizza std::coutogni collegamento della catena di espressioni verrebbe visualizzato separatamente in stdout; tra di loro può esserci qualche altro thread in scrittura a stdoutcausa del quale l'ordine dell'output finale viene incasinato.
legends2k

Risposte:


106

Lo standard C ++ 03 non dice nulla al riguardo. Quando non hai garanzie sulla thread-safe di qualcosa, dovresti trattarlo come non thread-safe.

Di particolare interesse qui è il fatto che coutè tamponato. Anche se le chiamate awrite è garantito (o qualsiasi altra cosa che compie quell'effetto in quella particolare implementazione) si escludano a vicenda, il buffer potrebbe essere condiviso dai diversi thread. Ciò porterà rapidamente alla corruzione dello stato interno del flusso.

E anche se l'accesso al buffer è garantito per essere thread-safe, cosa pensi che accadrà in questo codice?

// in one thread
cout << "The operation took " << result << " seconds.";

// in another thread
cout << "Hello world! Hello " << name << "!";

Probabilmente vuoi che ogni riga qui agisca in mutua esclusione. Ma come può garantirlo un'implementazione?

In C ++ 11 abbiamo alcune garanzie. L'FDIS dice quanto segue in §27.4.1 [iostream.objects.overview]:

L'accesso simultaneo alle funzioni di input formattato e non formattato di un oggetto iostream standard sincronizzato (§27.5.3.4) (§27.7.2.1) e di output (§27.7.3.1) o uno stream C standard da parte di più thread non deve comportare una gara di dati (§ 1.10). [Nota: gli utenti devono comunque sincronizzare l'uso simultaneo di questi oggetti e flussi da più thread se desiderano evitare caratteri interlacciati. - nota finale]

Quindi, non otterrai flussi danneggiati, ma devi comunque sincronizzarli manualmente se non vuoi che l'output sia spazzatura.


2
Tecnicamente vero per C ++ 98 / C ++ 03, ma penso che tutti lo sappiano. Ma questo non risponde alle due domande interessanti: che dire di C ++ 0x? Cosa implementazioni tipiche in realtà lo fanno ?
Nemo

1
@ edA-qa mort-ora-y: No, ti sei sbagliato. C ++ 11 definisce chiaramente che gli oggetti stream standard possono essere sincronizzati e mantenere un comportamento ben definito, non che lo siano per impostazione predefinita.
ildjarn

12
@ildjarn - No, @ edA-qa mort-ora-y è corretto. Finché cout.sync_with_stdio()è vero, l'utilizzo coutdi caratteri da più thread senza ulteriore sincronizzazione è ben definito, ma solo a livello di singoli byte. Pertanto, cout << "ab";ed cout << "cd"eseguito in thread diversi, può essere generato acdb, ad esempio, ma non può causare un comportamento indefinito.
JohannesD

4
@JohannesD: Siamo d'accordo su questo: è sincronizzato con l'API C sottostante. Il punto è che non è "sincronizzato" in modo utile, cioè è ancora necessaria la sincronizzazione manuale se non si vogliono dati spazzatura.
ildjarn

2
@ildjarn, sto bene con i dati spazzatura, quel bit lo capisco. Sono solo interessato alla condizione di gara dei dati, che ora sembra essere chiara.
edA-qa mort-ora-y

16

Questa è un'ottima domanda.

Primo, C ++ 98 / C ++ 03 non ha il concetto di "thread". Quindi in quel mondo la domanda non ha senso.

Che mi dici di C ++ 0x? Vedi la risposta di Martinho (che ammetto mi ha sorpreso).

Che ne dici di implementazioni specifiche pre-C ++ 0x? Bene, ad esempio, ecco il codice sorgente basic_streambuf<...>:sputcdi GCC 4.5.2 (intestazione "streambuf"):

 int_type
 sputc(char_type __c)
 {
   int_type __ret;
   if (__builtin_expect(this->pptr() < this->epptr(), true)) {
       *this->pptr() = __c;
        this->pbump(1);
        __ret = traits_type::to_int_type(__c);
      }
    else
        __ret = this->overflow(traits_type::to_int_type(__c));
    return __ret;
 }

Chiaramente, questo non esegue alcun blocco. E nemmeno lo fa xsputn. E questo è sicuramente il tipo di streambuf che usa cout.

Per quanto ne so, libstdc ++ non esegue alcun blocco attorno a nessuna delle operazioni di flusso. E non me lo aspetto, perché sarebbe lento.

Quindi, con questa implementazione, è ovviamente possibile che l'output di due thread si corrompa a vicenda ( non solo interleave).

Questo codice potrebbe danneggiare la struttura dei dati stessa? La risposta dipende dalle possibili interazioni di queste funzioni; ad esempio, cosa succede se un thread tenta di svuotare il buffer mentre un altro tenta di chiamarexsputn o qualsiasi altra cosa. Potrebbe dipendere da come il compilatore e la CPU decidono di riordinare i carichi e gli archivi di memoria; ci vorrebbe un'attenta analisi per essere sicuri. Dipende anche da cosa fa la CPU se due thread tentano di modificare la stessa posizione contemporaneamente.

In altre parole, anche se dovesse funzionare correttamente nell'ambiente corrente, potrebbe interrompersi quando aggiorni uno dei tuoi runtime, compilatore o CPU.

Riepilogo esecutivo: "Non lo farei". Crea una classe di registrazione che esegua il blocco corretto o passa a C ++ 0x.

Come alternativa debole, potresti impostare cout su unbuffered. È probabile (anche se non garantito) che salti tutta la logica relativa al buffer e chiami writedirettamente. Anche se potrebbe essere proibitivo.


1
Buona risposta, ma guarda la risposta di Martinho che mostra che C ++ 11 definisce la sincronizzazione per cout.
edA-qa mort-ora-y

7

Lo standard C ++ non specifica se la scrittura nei flussi è thread-safe, ma di solito non lo è.

www.techrepublic.com/article/use-stl-streams-for-easy-c-plus-plus-thread-safe-logging

e inoltre: I flussi di output standard in C ++ sono thread-safe (cout, cerr, clog)?

AGGIORNARE

Dai un'occhiata alla risposta di @Martinho Fernandes per sapere cosa dice al riguardo il nuovo standard C ++ 11.


3
Immagino che dal momento che C ++ 11 è ora lo standard, questa risposta è attualmente sbagliata.
edA-qa mort-ora-y

6

Come menzionato in altre risposte, questo è sicuramente specifico del fornitore poiché lo standard C ++ non fa menzione del threading (questo cambia in C ++ 0x).

GCC non fa molte promesse sulla sicurezza dei thread e sull'I / O. Ma la documentazione per ciò che promette è qui:

la cosa fondamentale è probabilmente:

Il tipo __basic_file è semplicemente una raccolta di piccoli wrapper attorno al livello C stdio (di nuovo, vedere il collegamento sotto Struttura). Non ci blocchiamo, ma passiamo semplicemente alle chiamate a fopen, fwrite e così via.

Quindi, per 3.0, la domanda "il multithreading è sicuro per I / O" deve essere risolta con "la libreria C della tua piattaforma è sicura per i thread I / O?" Alcuni lo sono per impostazione predefinita, altri no; molti offrono più implementazioni della libreria C con diversi compromessi in termini di sicurezza ed efficienza dei thread. Tu, il programmatore, sei sempre tenuto a prenderti cura di più thread.

(Ad esempio, lo standard POSIX richiede che le operazioni FILE * di C stdio siano atomiche. Le librerie C conformi a POSIX (ad esempio, su Solaris e GNU / Linux) hanno un mutex interno per serializzare le operazioni sui FILE *. Tuttavia, è ancora necessario per non fare cose stupide come chiamare fclose (fs) in un thread seguito da un accesso di fs in un altro.)

Quindi, se la libreria C della tua piattaforma è threadsafe, le tue operazioni di I / O fstream saranno threadsafe al livello più basso. Per operazioni di livello superiore, come la manipolazione dei dati contenuti nelle classi di formattazione del flusso (ad esempio, l'impostazione di callback all'interno di uno std :: ofstream), è necessario proteggere tali accessi come qualsiasi altra risorsa condivisa critica.

Non so se qualcosa sia cambiato nel periodo di tempo 3.0 menzionato.

La documentazione sulla sicurezza dei thread di MSVC iostreamspuò essere trovata qui: http://msdn.microsoft.com/en-us/library/c9ceah3b.aspx :

Un singolo oggetto è thread-safe per la lettura da più thread. Ad esempio, dato un oggetto A, è sicuro leggere A dal thread 1 e dal thread 2 contemporaneamente.

Se un singolo oggetto viene scritto da un thread, tutte le letture e le scritture su quell'oggetto sullo stesso o su altri thread devono essere protette. Ad esempio, dato un oggetto A, se il thread 1 sta scrivendo su A, è necessario impedire al thread 2 di leggere o scrivere su A.

È sicuro leggere e scrivere in un'istanza di un tipo anche se un altro thread sta leggendo o scrivendo in un'istanza diversa dello stesso tipo. Ad esempio, dati gli oggetti A e B dello stesso tipo, è sicuro se A viene scritto nel thread 1 e B viene letto nel thread 2.

...

Classi iostream

Le classi iostream seguono le stesse regole delle altre classi, con un'eccezione. È sicuro scrivere su un oggetto da più thread. Ad esempio, il thread 1 può scrivere su cout contemporaneamente al thread 2. Tuttavia, ciò può comportare che l'output dei due thread venga mischiato.

Nota: la lettura da un buffer di flusso non è considerata un'operazione di lettura. Dovrebbe essere considerata come un'operazione di scrittura, perché cambia lo stato della classe.

Notare che queste informazioni si riferiscono alla versione più recente di MSVC (attualmente per VS 2010 / MSVC 10 / cl.exe16.x). È possibile selezionare le informazioni per le versioni precedenti di MSVC utilizzando un controllo a discesa nella pagina (e le informazioni sono diverse per le versioni precedenti).


1
"Non so se qualcosa sia cambiato durante il periodo di tempo 3.0 menzionato." Sicuramente lo ha fatto. Negli ultimi anni, l'implementazione dei flussi g ++ ha eseguito il proprio buffering.
Nemo
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.