std::stringstream arbeitet unzuverlässig



  • Hallo,

    ich habe folgenden Unit-Test-Code mit dem ich Befehle an meine Schach-Engine schicke:

    TEST(UCICommunicationTest, ManyMoves)
    {
        std::stringstream inputStream;
        std::stringstream outputStream;
        std::stringstream errorStream;
    
        UCICommunication uciCom(inputStream, outputStream, errorStream);
    
        std::thread communicationThread([&uciCom]{
            uciCom.startCommunication();
        });
    
        constexpr auto fenString = "position startpos moves a2a4 ";
    
        inputStream << fenString << "\n";
        inputStream << "go depth 6\n" << std::flush;
        std::this_thread::sleep_for(1s);
        inputStream << "quit\n" << std::flush;
        communicationThread.join();
        // ...
    }
    

    Die Kommunikations-Schnittstelle nimmt die abstrakten Datentypen std::istream und std::ostream entgegen, sodass ich bei Unit-Tests mit std::stringstream arbeiten und testen kann und beim Produktiv-Code mit std::cout und std::cin:

    UCICommunication::UCICommunication(std::istream &inputStream, std::ostream &outputStream, std::ostream &errorStream) :
                m_inputStream(inputStream),
                m_outputStream(outputStream),
                m_errorStream(errorStream),
                m_timeToSearch(std::numeric_limits<int64_t>::max()),
                m_searchThread(&UCICommunication::searchBestMove, this) // Für diese Methode nutze ich einen Thread!
        {}
    

    Das hier ist die startCommunication() Methode, die sowohl vom Test- als auch Produktiv-Code aufgerufen wird:

    void UCICommunication::startCommunication()
    {
        registerToUI();
    
        std::string uiCommand;
    
        for (getInput(uiCommand); ;getInput(uiCommand))
        {
            // make sure uiCommand is available
            UCIParser parser(uiCommand);
    
            if (parser.uiQuitGame())
            {
                std::cout << "Game quit!!!!" << std::endl;
                quitGame();
                break;
            }
    
            // ...
            else if (parser.uiHasSentGoCommand())
            {
                executeGoCommand(parser);
            }
        }
    }
    
    void UCICommunication::getInput(std::string &uiCommand)
    {
        std::getline(m_inputStream, uiCommand);
    }
    

    Das Problem ist nun, sobald ich Threads in der UCICommunication-Klasse verwende, kommen die Kommandos nicht mehr an, obwohl ich den Thread nicht beim Parsen, sondern nur bei der Suche verwende. Lasse ich den Thread weg, funktioniert alles zuverlässig.
    Was auch zuverlässig funktioniert ist, wenn ich mit einer GUI über std::cout und std::cin kommuniziere. Nur meine Tests hängen beim join() des communicationThread (siehe letzte Zeile beim Test) fest.

    Hat jemand eine Idee, weshalb die Befehle verschluckt werden? Muss ich noch irgendeinen Buffer leeren, oder so?

    Falls jemand den Code kompilieren und ausführen möchte: Hier ist der Test bzw. das Repository.
    Die einzige Abhängigkeit die das Projekt hat, ist Google Test.

    Danke im Voraus für Vorschläge!
    Steffo



  • Hast du denn mal bei dem Testprojekt mit dem Debugger geschaut, wo die Schleife in der startCommunication() hängt?
    So wie ich den Code verstehe, wird die Schleife (und damit die Funktion) doch nur bei einem "quit" verlassen. Du solltest evtl. noch Fehlerflags des Streams abfragen.

    Und was passiert, wenn du erst den inputStream befüllst und dann den communicationThread startest?



  • Hi @Steffo , ich würde dir empfehlen, den Stockfish port for Java zu verwenden ( http://www.rahular.com/stockfish-port-for-java/ ), und ganz auf das C++ Geraffel zu verzichten, denn damit wirst du wahrscheinlich nicht glücklich... 😉

    Bilderkennung, etc. mache ich auch alles mit Java, und das funktioniert gut!



  • @Fragender sagte in std::stringstream arbeitet unzuverlässig:

    Hi @Steffo , ich würde dir empfehlen, den Stockfish port for Java zu verwenden ( http://www.rahular.com/stockfish-port-for-java/ ), und ganz auf das C++ Geraffel zu verzichten, denn damit wirst du wahrscheinlich nicht glücklich... 😉

    Bilderkennung, etc. mache ich auch alles mit Java, und das funktioniert gut!

    Woher nimmst du die Info, dass Steffo Stockfish verwendet bzw. verwenden will?

    Wenn man eine eigene Chess Engine baut, dann ergibt es nicht so viel Sinn, Stockfish zu nutzen 😃



  • @Th69 sagte in std::stringstream arbeitet unzuverlässig:

    Hast du denn mal bei dem Testprojekt mit dem Debugger geschaut, wo die Schleife in der startCommunication() hängt?
    So wie ich den Code verstehe, wird die Schleife (und damit die Funktion) doch nur bei einem "quit" verlassen. Du solltest evtl. noch Fehlerflags des Streams abfragen.

    Und was passiert, wenn du erst den inputStream befüllst und dann den communicationThread startest?

    Wenn ich den Debugger einfach starte und ein paar Sekunden abwarte und dann den Prozess pausiere, dann liest inputStream nur noch leere Zeichen ein.
    Bin ich aber mit dem Debugger von Anfang an dabei und parse die einzelnen Inputs, dann verhält sich alles ganz normal. - Hat etwas von einem Heisen-Bug: Schaue ich zu, verhält sich alles normal. Lasse ich alles laufen, hängt sich das Programm auf, da das "quit" Kommando nicht verarbeitet wurde.
    Das Bad-Bit wird nicht gesetzt, allerdings das Fail-Bit, was wohl daran liegt, dass irgendwann kein weiterer Input kommt.

    @Fragender Du bist echt eine lustige Nase! Ich schreibe meine eigene Schach-Engine für den Lern-Effekt und dieser "Stockfish-Port" ist keiner, sondern er kommuniziert einfach nur über wenige Zeilen Java-Code mit Stockfish, der übrigens in C++ geschrieben ist!



  • Vielleicht ist das Problem ist, dass std::stringstream nicht thread-safe ist und std::cout und std::cin schon und das deshalb mit Produktiv-Code funktioniert.
    Im Test schreibe ich vom Main-Thread in den communicationThread. Dieser Thread sieht nicht unbedingt die neuesten Updates.
    Dass das alles funktioniert, wenn ich den m_searchThread nicht verwende, kann reiner Zufall sein bzw. damit zu tun haben, dass ich den sleep nicht drin habe und das irgendwie auf den Datensichtbarkeit auswirkt.
    Die Frage ist nun: Wie kann man das nun mit Unit-Tests testen?



  • @Leon0402 Hast recht. Ich hatte nicht alles gelesen, aber für mich sah es so aus, als wollte er einen FEN-String an eine Schach-Engine weitergeben und da hatte ich an Stockfish bzw. UCI gedacht.



  • @Steffo sagte in std::stringstream arbeitet unzuverlässig:

    Vielleicht ist das Problem ist, dass std::stringstream nicht thread-safe ist und std::cout und std::cin schon und das deshalb mit Produktiv-Code funktioniert.
    Im Test schreibe ich vom Main-Thread in den communicationThread. Dieser Thread sieht nicht unbedingt die neuesten Updates.
    Dass das alles funktioniert, wenn ich den m_searchThread nicht verwende, kann reiner Zufall sein bzw. damit zu tun haben, dass ich den sleep nicht drin habe und das irgendwie auf den Datensichtbarkeit auswirkt.
    Die Frage ist nun: Wie kann man das nun mit Unit-Tests testen?

    jaein std::cout/std::cin sind thread safe bezogen auf "data race" aber nicht bezogen auf "race condition".
    Wobei das nur explizit für std::cin/cout/clog gilt. Für restliche streams nicht!

    z.b. in diesem abschnitt in deinem code

    inputStream << fenString << "\n";
    

    Wenn inputstream "std::cout" wäre, dann ist nur garantiert, dass der "fenstring" part atomar ausgegeben wird, aber bevor der "\n" part ausgegeben wird, kann ein anderer thread was ausgeben.

    Das andere ist, dass im falle von std::cin der aufruf des >> operators blockiert, wenn keine Daten im stream sind. Beim std::stringstream muss das aber nicht der fall sein (konnte jetzt keine Information dazu finden ob das stimmt oder nicht)

    Aber generell sollte man den zugriff auf eine shared ressource, wie in deinem beispiel der stringstream synchronisieren.
    Und zusätzlich solltest du prüfen ob der stream nicht in einen Fehlerzustand gegangen ist.
    Weil dann, wenn ich mich recht entsinne, keinerlei ein/ausgaben mehr erfolgen können.

    Nach meinem Verständnis sieht es so aus, als ob dein code implizit erwartet, dass der inputstream blockiert, wenn keine Daten vorhanden sind (wie das bei std::cin der fall ist)
    Daher funktioniert der code auch nur mit einem inputstream (std::istream), welcher so ein verhalten aufweist.

    Das gleiche Problem hast du in deinem Unit test code wo du auf das Ergebnis der engine warten möchtest, welche via outputStream/errorStream ausgegeben wird.

    Daher solltest du überlegen ob du die eingabe von befehlen komplett von der Verarbeitung dieser trennst.
    Das gleiche gilt natürlich auch für die Verwendung der anderen streams.

    Eine Randbemerkung. In deiner UCICommunication::startCommunication() Methode hast du ein std::cout drinn, wodurch du diese ausgabe der engine auch nicht testen kannst.



  • @firefly: Ja, das sehe ich genauso.
    Daher habe ich ja auch

    Und was passiert, wenn du erst den inputStream befüllst und dann den communicationThread startest?

    geschrieben.
    Ich sehe beim Unittest aber auch keinen Sinn dadrin, überhaupt noch einen Thread zu benutzen, sondern einfach direkt hintereinander:

    inputStream << fenString << "\n";
    // ...
    inputStream << "quit\n" << std::flush;
    
    UCICommunication uciCom(inputStream, outputStream, errorStream);
    uciCom.startCommunication();
    

    Du willst ja die Logik testen und nicht das Terminalverhalten.



  • Wenn ich folgende bei Änderungen hinzufüge, funktioniert es, aber keine diese Änderungen funktioniert alleine, sondern man braucht beide!

    void UCICommunication::startCommunication()
    {
        registerToUI();
    
        std::string uiCommand;
    
        for (getInput(uiCommand); ;getInput(uiCommand))
        {
            if (uiCommand.empty()) // Neu hinzu
            {
                continue;
            }
            // make sure uiCommand is available
            UCIParser parser(uiCommand);
            // ...
        }
    }
    
    void UCICommunication::getInput(std::string &uiCommand)
    {
        m_inputStream.clear(); // Clear errors: Neu hinzu
        std::getline(m_inputStream, uiCommand);
    }
    

    @Th69 Deinen Vorschlag kann ich so nicht umsetzen, da der Thread sofort beendet wird, bevor überhaupt angefangen wird zu rechnen.



  • Ok, wie sich herausstellt, funktioniert der Ansatz in vielen Fällen, aber nicht zu 100 %. 😑


Anmelden zum Antworten