2012-03-02 15 views
8

Pracuję z jakimś wielowątkowym kodem do projektu gry, i mam trochę dość sortowania przez standardowe wymioty utworzone przez dwa wątki używające cout do debugowania wiadomości w tym samym czasie. Zrobiłem trochę badań i przez godzinę lub dwie wpatrywałem się w ścianę, zanim wymyśliłem "coś". Poniższy kod używa SFML do przechowywania i wątkowania. Muteksy SFML są właśnie zapakowane w krytyczne sekcje w oknach.Technika cout safe. Czy czegoś brakuje?

Header:

#include <SFML\System.hpp> 
#include <iostream> 

class OutputStreamHack 
{ 
    public: 
    OutputStreamHack(); 
    ~OutputStreamHack(); 

    ostream& outputHijack(ostream &os); 

    private: 
    sf::Clock myRunTime; 
    sf::Mutex myMutex; 
}; 

static OutputStream OUTHACK; 

ostream& operator<<(ostream& os, const OutputStreamHack& inputValue); 

realizacji:

#include <SFML\System.hpp> 
#include <iostream> 

#include "OutputStreamHack.h" 

using namespace std; 

OutputStreamHack::OutputStreamHack() 
{ 
    myMutex.Unlock(); 
    myRunTime.Reset(); 
} 

OutputStreamHack::~OutputStreamHack() 
{ 
    myMutex.Unlock(); 
    myRunTime.Reset(); 
} 

ostream& OutputStreamHack::outputHijack(ostream &os) 
{ 

    sf::Lock lock(myMutex); 
    os<<"<"<<myRunTime.GetElapsedTime()<<","<<GetCurrentThreadId()<<"> "<<flush; 
    return os; 
} 

ostream& operator<<(ostream& os, const OutputStreamHack& inputValue) 
{ 
    OUTHACK.outputHijack(os); 
    return os; 
} 

Zastosowanie:

cout<<OUTHACK<<val1<<val2<<val3....<<endl; 

Ok, sposób działania jest przez przeciążony operatora wkładania, która nakłada bezpieczeństwa wątku blokując iterator w statycznym obiekcie, a następnie przepłukanie bufora. Jeśli poprawnie rozumiem ten proces (jestem w większości programistą samoukiem), cout przetwarza elementy łańcucha wstawiania od końca do początku, przekazując zmienną ostream w dół łańcucha dla każdego elementu, który ma być dodany do strumienia. Po dotarciu do elementu OUTHACK zostaje wywołany przeciążony operator, muteks jest zablokowany, a strumień jest przepłukiwany.

Dodałem informacje o debugowaniu identyfikatora czasu/wątku do danych wyjściowych w celu weryfikacji. Jak dotąd moje testy pokazują, że ta metoda działa. Mam kilka wątków walących cout z wieloma argumentami, a wszystko wychodzi w odpowiedniej kolejności.

Z tego, co przeczytałem podczas badania tego problemu, brak bezpieczeństwa wątków w cout wydaje się być dość powszechnym problemem, z którym borykają się ludzie, wchodząc w programowanie gwintowe. Próbuję się dowiedzieć, czy technika, której używam, jest prostym rozwiązaniem problemu, czy też myślę, że jestem sprytny, ale brakuje mi czegoś ważnego.

Z mojego doświadczenia wynika, że ​​słowo "sprytny" użyte do opisania programowania jest po prostu hasłem opóźnionego bólu. Czy coś tu jest, czy po prostu gonisz kiepskie hacki w kółko?

Dzięki!

+1

Fakt, że działa, to czysta przyjemność. Tylko wynik identyfikatora czasu i wątku jest chroniony przez muteks. Jest całkiem możliwe, aby inny wątek wkradł się pomiędzy 'OUTHACK' i' val1'. –

+1

Najpierw rozważ pisanie do 'ostringstream', a następnie zrzut zawartości w jednej operacji do' cout'. 'cout' będzie generalnie bezpieczny dla wątków, po prostu blokowanie jest oczywiście dla każdego połączenia, a każda operacja' << 'jest odrębnym połączeniem .... W ten sposób nie ma dodatkowego blokowania w kodzie aplikacji - co może tylko zmniejszyć równoległość. –

+0

Powiązane: [Czy synchronizacja/wątek jest bezpieczny?] (Http://stackoverflow.com/questions/6374264/is-cout-synchronized-thread-safe/6374525#6374525) – legends2k

Odpowiedz

19

Co nie jest tutaj, wątki bezpieczne nie jest cout per se. Wywołuje dwa wywołania funkcji w sekwencji. std::cout << a << b jest mniej więcej równoznaczne z wywoływaniem operator<<(std::cout, a), a następnie operator<<(std::cout, b). Wywołanie dwóch funkcji w sekwencji nie daje żadnej gwarancji, że zostaną wykonane w sposób atomowy.

W istocie tylko dane wyjściowe identyfikatora czasu i wątku są chronione przez muteks. Jest całkiem możliwe, że kolejny wątek wkradnie się pomiędzy wstawienie OUTHACK i val1, ponieważ blokada nie jest już trzymana po włożeniu OUTHACK.

Możesz mieć operator<< dla swojego OutputStreamHack powrotu według wartości obiektu, który odblokowuje w destruktorze. Ponieważ pliki tymczasowe istnieją do końca każdego pełnego wyrażenia, kod będzie blokował blokadę "aż do średnika". Jednakże, ponieważ kopie mogą być zaangażowane, może to być problematyczne bez konstruktora ruchu (lub niestandardowego konstruktora kopiowania w C++ 03, podobnego do auto_ptr 's sapanie).

Inną opcją jest użycie istniejącego bezpieczeństwa wątków z cout (gwarantowanego przez język w C++ 11, ale wiele implementacji było wcześniej chronionych wątkami). Utwórz obiekt, który przesyła wszystko do elementu std::stringstream, a następnie zapisz go na raz, gdy zostanie zniszczony.

class FullExpressionAccumulator { 
public: 
    explicit FullExpressionAccumulator(std::ostream& os) : os(os) {} 
    ~FullExpressionAccumulator() { 
     os << ss.rdbuf() << std::flush; // write the whole shebang in one go 
    } 

    template <typename T> 
    FullExpressionAccumulator& operator<<(T const& t) { 
     ss << t; // accumulate into a non-shared stringstream, no threading issues 
     return *this; 
    } 

private: 
    std::ostream& os; 
    std::stringstream ss; 

    // stringstream is not copyable, so copies are already forbidden 
}; 

// using a temporary instead of returning one from a function avoids any issues with copies 
FullExpressionAccumulator(std::cout) << val1 << val2 << val3; 
+2

Ah, widzę różnicę w moim sposobie myślenia . Działałem przy założeniu, że zachowanie cout było rekurencyjne, a nie iteracyjne, i że pracowało nad strumieniem lokalnym, który został scalony w standardowe wyjście w punkcie, w którym przetwarzano OUTHACK. Twoja technika wygląda dokładnie to, czego mi brakowało! Dzięki za tonę. – Chris