Czy cout jest zsynchronizowany / bezpieczny dla wątków?

112

Generalnie zakładam, że strumienie nie są zsynchronizowane, to do użytkownika należy odpowiednie zablokowanie. Czy jednak takie rzeczy jak coutbiblioteka standardowa są traktowane w specjalny sposób?

Oznacza to, że jeśli wiele wątków pisze do, coutczy mogą uszkodzić coutobiekt? Rozumiem, że nawet po zsynchronizowaniu nadal otrzymujesz wyjście z przeplotem losowo, ale czy to przeplot jest gwarantowane. To znaczy, czy można bezpiecznie używać coutz wielu wątków?

Czy ten dostawca jest zależny? Co robi GCC?


Ważne : jeśli powiesz „tak”, podaj jakieś odniesienie do odpowiedzi, ponieważ potrzebuję jakiegoś dowodu na to.

Nie martwię się również o podstawowe wywołania systemowe, te są w porządku, ale strumienie dodają warstwę buforowania na wierzchu.

edA-qa mort-ora-y
źródło
2
To zależy od dostawcy. C ++ (przed C ++ 0x) nie ma pojęcia wielu wątków.
Sven
2
A co z C ++ 0x? Definiuje model pamięci i czym jest wątek, więc może te rzeczy wyciekły na wyjściu?
rubenvb
2
Czy są jacyś dostawcy, którzy zapewniają bezpieczeństwo wątków?
edA-qa mort-ora-y
Czy ktoś ma link do najnowszego standardu zaproponowanego w C ++ 2011?
edA-qa mort-ora-y
4
W pewnym sensie jest to sytuacja, w której printfświeci, gdy cały wynik jest zapisywany stdoutw jednym ujęciu; podczas korzystania z std::coutkażdego łącza łańcucha wyrażeń zostanie wyprowadzony osobno stdout; Pomiędzy nimi może znajdować się inny wątek, w stdoutwyniku którego kolejność końcowego wyjścia zostanie zepsuta.
legends2k

Odpowiedzi:

106

Standard C ++ 03 nic o tym nie mówi. Gdy nie masz gwarancji, że coś jest bezpieczne dla wątków, powinieneś traktować to jako niechronione wątkowo.

Szczególnie interesujący jest tutaj fakt, że coutjest buforowany. Nawet jeśli wywołania write(lub cokolwiek to jest, co powoduje ten efekt w tej konkretnej implementacji) są wzajemnie wykluczające się, bufor może być współużytkowany przez różne wątki. To szybko doprowadzi do zepsucia wewnętrznego stanu strumienia.

A nawet jeśli gwarantuje się, że dostęp do bufora będzie bezpieczny dla wątków, jak myślisz, co się stanie w tym kodzie?

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

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

Prawdopodobnie chcesz, aby każda linia tutaj działała we wzajemnym wykluczeniu. Ale jak wdrożenie może to zagwarantować?

W C ++ 11 mamy pewne gwarancje. FDIS mówi, co następuje w §27.4.1 [iostream.objects.overview]:

Jednoczesny dostęp do zsynchronizowanego (§27.5.3.4) standardowego, sformatowanego i niesformatowanego wejścia obiektu iostream (§27.7.2.1) oraz wyjścia (§27.7.3.1) funkcji lub standardowego strumienia C przez wiele wątków nie powinien powodować wyścigu danych (§ 1.10). [Uwaga: użytkownicy muszą nadal synchronizować jednoczesne korzystanie z tych obiektów i strumieni przez wiele wątków, jeśli chcą uniknąć przeplatanych znaków. - notatka końcowa]

Nie otrzymasz więc uszkodzonych strumieni, ale nadal musisz zsynchronizować je ręcznie, jeśli nie chcesz, aby dane wyjściowe były śmieciami.

R. Martinho Fernandes
źródło
2
Technicznie prawda dla C ++ 98 / C ++ 03, ale myślę, że każdy o tym wie. Ale to nie odpowiada na dwa interesujące pytania: A co z C ++ 0x? Co właściwie robią typowe wdrożenia ?
Nemo
1
@ edA-qa mort-ora-y: Nie, mylisz się. C ++ 11 jasno definiuje, że standardowe obiekty strumienia mogą być zsynchronizowane i zachowują dobrze zdefiniowane zachowanie, a nie że są one domyślnie.
ildjarn
12
@ildjarn - Nie, @ edA-qa mort-ora-y jest poprawne. O ile cout.sync_with_stdio()jest prawdą, użycie coutdo wyprowadzania znaków z wielu wątków bez dodatkowej synchronizacji jest dobrze zdefiniowane, ale tylko na poziomie pojedynczych bajtów. W ten sposób cout << "ab";i cout << "cd"wykonywane w różnych wątkach mogą acdbna przykład wyprowadzać , ale nie mogą powodować niezdefiniowanego zachowania.
JohannesD
4
@JohannesD: Zgadzamy się - jest zsynchronizowany z bazowym API C. Chodzi mi o to, że nie jest "zsynchronizowany" w użyteczny sposób, tj. Nadal potrzebna jest ręczna synchronizacja, jeśli nie chcą danych śmieciowych.
ildjarn
2
@ildjarn, jestem w porządku z danymi śmieci, to trochę rozumiem. Interesuje mnie tylko stan wyścigu danych, który wydaje się teraz jasny.
edA-qa mort-ora-y
16

To świetne pytanie.

Po pierwsze, w C ++ 98 / C ++ 03 nie ma pojęcia „wątku”. Więc w tym świecie pytanie jest bez znaczenia.

A co z C ++ 0x? Zobacz odpowiedź Martinho (co, przyznaję, mnie zaskoczyło).

A co z konkretnymi implementacjami sprzed C ++ 0x? Na przykład, oto kod źródłowy basic_streambuf<...>:sputcz GCC 4.5.2 (nagłówek „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;
 }

Oczywiście nie powoduje to blokowania. I też nie xsputn. I to jest zdecydowanie rodzaj streambuf, którego używa cout.

O ile wiem, libstdc ++ nie blokuje żadnych operacji na strumieniu. I nie spodziewałbym się żadnego, ponieważ byłoby to powolne.

Tak więc przy tej implementacji jest oczywiście możliwe, że wyjście dwóch wątków może się wzajemnie uszkodzić (a nie tylko przeplatać).

Czy ten kod może uszkodzić samą strukturę danych? Odpowiedź zależy od możliwych interakcji tych funkcji; np. co się stanie, jeśli jeden wątek spróbuje opróżnić bufor, podczas gdy inny spróbuje wywołać xsputnlub cokolwiek. Może to zależeć od tego, jak kompilator i procesor zdecydują się zmienić kolejność ładowania i przechowywania pamięci; wymagałoby dokładnej analizy, aby mieć pewność. Zależy to również od tego, co robi twój procesor, jeśli dwa wątki próbują modyfikować tę samą lokalizację jednocześnie.

Innymi słowy, nawet jeśli zdarza się, że działa dobrze w obecnym środowisku, może się zepsuć podczas aktualizacji dowolnego środowiska wykonawczego, kompilatora lub procesora.

Streszczenie: „Nie chciałbym”. Zbuduj klasę rejestrowania, która wykonuje odpowiednie blokowanie lub przejdź do C ++ 0x.

Jako słabą alternatywę możesz ustawić cout na niebuforowany. Jest prawdopodobne (choć nie jest to gwarantowane), że pominie całą logikę związaną z buforem i wywoła writebezpośrednio. Chociaż może to być zbyt powolne.

Nemo
źródło
1
Dobra odpowiedź, ale spójrz na odpowiedź Martinho, która pokazuje, że C ++ 11 definiuje synchronizację cout.
edA-qa mort-ora-y
7

Standard C ++ nie określa, czy zapis do strumieni jest bezpieczny dla wątków, ale zwykle tak nie jest.

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

a także: czy standardowe strumienie wyjściowe w C ++ są bezpieczne wątkowo (cout, cerr, clog)?

AKTUALIZACJA

Proszę spojrzeć na odpowiedź @Martinho Fernandes, aby dowiedzieć się, co mówi o tym nowy standard C ++ 11.

phoxis
źródło
3
Wydaje mi się, że ponieważ C ++ 11 jest teraz standardem, ta odpowiedź jest teraz właściwie błędna.
edA-qa mort-ora-y
6

Jak wspominają inne odpowiedzi, jest to zdecydowanie specyficzne dla dostawcy, ponieważ standard C ++ nie wspomina o wątkowaniu (to zmienia się w C ++ 0x).

GCC nie składa wielu obietnic dotyczących bezpieczeństwa wątków i operacji we / wy. Ale dokumentacja tego, co obiecuje, jest tutaj:

kluczowa sprawa to prawdopodobnie:

Typ __basic_file jest po prostu zbiorem małych opakowań wokół warstwy C stdio (ponownie, zobacz łącze w sekcji Struktura). Nie blokujemy się, ale po prostu przechodzimy do wywołań fopen, fwrite i tak dalej.

Tak więc w wersji 3.0 na pytanie „czy wielowątkowość jest bezpieczna dla operacji we / wy” należy odpowiedzieć „czy biblioteka C platformy jest bezpieczna dla wątków we / wy?” Niektóre są domyślne, inne nie; wiele z nich oferuje wiele implementacji biblioteki C z różnymi kompromisami w zakresie bezpieczeństwa wątków i wydajności. Ty, programista, zawsze musisz zajmować się wieloma wątkami.

(Na przykład standard POSIX wymaga, aby operacje C stdio FILE * były atomowe. Biblioteki C zgodne z POSIX (np. W systemie Solaris i GNU / Linux) mają wewnętrzny mutex do serializacji operacji na PLIKU * s. Jednak nadal potrzebujesz nie robić głupich rzeczy, takich jak wywoływanie fclose (fs) w jednym wątku, a następnie dostęp do fs w innym).

Tak więc, jeśli biblioteka C platformy jest wątkowo bezpieczna, wówczas operacje we / wy fstream będą bezpieczne wątkowo na najniższym poziomie. W przypadku operacji wyższego poziomu, takich jak manipulowanie danymi zawartymi w klasach formatowania strumienia (np. Konfigurowanie wywołań zwrotnych w std :: ofstream), należy chronić takie dostępy jak każdy inny krytyczny zasób współdzielony.

Nie wiem, czy cokolwiek się zmieniło od wspomnianych ram czasowych 3.0.

Dokumentację MSVC dotyczącą bezpieczeństwa wątków dla iostreamsmożna znaleźć tutaj: http://msdn.microsoft.com/en-us/library/c9ceah3b.aspx :

Pojedynczy obiekt jest bezpieczny dla wątków do odczytu z wielu wątków. Na przykład, mając obiekt A, można bezpiecznie odczytać A z wątku 1 iz wątku 2 jednocześnie.

Jeśli pojedynczy obiekt jest zapisywany przez jeden wątek, wszystkie odczyty i zapisy do tego obiektu w tym samym lub innych wątkach muszą być chronione. Na przykład, biorąc pod uwagę obiekt A, jeśli wątek 1 pisze do A, to wątek 2 musi być chroniony przed odczytem lub zapisem do A.

Bezpieczne jest odczytywanie i zapisywanie w jednym wystąpieniu typu, nawet jeśli inny wątek odczytuje lub zapisuje do innego wystąpienia tego samego typu. Na przykład przy danych obiektach A i B tego samego typu jest bezpieczne, jeśli A jest zapisywane w wątku 1, a B jest odczytywane w wątku 2.

...

Klasy iostream

Klasy iostream podlegają tym samym regułom, co inne klasy, z jednym wyjątkiem. Zapisywanie do obiektu z wielu wątków jest bezpieczne. Na przykład, wątek 1 może pisać do cout w tym samym czasie co wątek 2. Może to jednak spowodować zmieszanie danych wyjściowych z dwóch wątków.

Uwaga: Odczyt z bufora strumienia nie jest uważany za operację odczytu. Należy to traktować jako operację zapisu, ponieważ zmienia to stan klasy.

Należy pamiętać, że te informacje dotyczą najnowszej wersji MSVC (obecnie dla VS 2010 / MSVC 10 / cl.exe16.x). Możesz wybrać informacje dla starszych wersji MSVC za pomocą rozwijanej kontrolki na stronie (a informacje są inne dla starszych wersji).

Michael Burr
źródło
1
„Nie wiem, czy cokolwiek się zmieniło od wspomnianych ram czasowych 3.0”. Zdecydowanie tak. Przez ostatnie kilka lat implementacja strumieni g ++ wykonywała własne buforowanie.
Nemo