RepeatingTasks beenden sich nicht richtig



  • Hallo Leute,

    in einem anderen Thread habe ich von SeppJ die RepeatingTasks bekommen, womit ich zyklisch Threads aufrufen kann.

    Jetzt habe ich aber das Problem, dass, wenn das Programm beendet wird, weil die Hardware z.B. sich herunterfährt, die zyklischen Threads mittels der stop()-Funktion wahrscheinlich nicht richtig beendet werden,
    was zu folgenden Fehlern führt:

    PROGRAM ERROR: [SIGSEGV] false memory access
    

    Mein Kollege hat folgende Vermutung:
    "Die RepeatingTasks sind auch eigenständige Threads, genauer gesagt sogar mindestens jeweils zwei: einer läuft dauernd in der execute_task()-Schleife, und der erzeugt alle Nase lang einen neuen Thread, der den jeweiligen Job abarbeitet.

    Das stop() sorgt zwar dafür, dass die execute_task()-Schleife abbricht, aber die von der Schleife gestarteten Threads laufen erst mal parallel weiter. Und je nachdem, auf welche Ressourcen dabei zugegriffen wird, kann es schon sein, dass einem der Threads beim Beenden der Applikation der Boden unter den Füßen weggezogen wird => SIGSEGV "

    Wie löst man das Problem richtig ?
    Wie bekomme ich die gestarteten Threads beendet ?

    Ursprünglicher Thread und RepeatingTask hier: 🙂
    https://www.c-plusplus.net/forum/335534

    Ich hoffe, ich habe mich verständlich und nachvollziehbar ausgedrückt 🙂
    Falls nicht, bitte bitte nochmal nachfragen, denn für mich als Nicht Außenstehender ist es einfacher das Geschriebene zu verstehen 🙂 🙄

    Danke für alle Hilfestellungen 👍


  • Mod

    Grundlegend gibt es zwei Möglichkeiten:
    1. Man wartet, bis alle Tasks fertig sind, bevor das Programm beendet wird. Das heißt konkret, dass man die Threads eben nicht detached, sondern joined.
    2. Man muss den Threads irgendeine Art von Signal schicken, dass sie sich beenden sollen. Die Tasks müssen regelmäßig auf dieses Signal prüfen und dann gegebenenfalls abbrechen. Wenn sich alle Tasks beendet haben (siehe 1.), kann man das Programm beenden.



  • Hallo SeppJ,

    danke für deine Antwort.

    1. hab ich auch schon probiert, aber dann steht das komplette Programm.
    Deswegen ist das detach toll, damit es unabhängig im Hintergrund läuft.
    Was wiederum natürlich doof ist, weil ich unabhängige nicht mehr steuern kann 😞

    2. Denkst du da an mutexe und condition variable ?
    Damit hab ich zuletzt im Studium zutun gehabt und nie komplett verstanden 😞
    Daran dachte ich auch schon, aber wüsste jetzt nicht, wie ich die da einarbeite, weil ständig neue Tasks erstellt werden, die dann unabhängig laufen.
    Ich weiß daher nicht, wie ich die "rufen" und beenden soll 😞

    Auf dem Gebiet bin ich totaler Newbie und die Erklärungen im Internet helfen mir leider gerade nicht weiter es zu verstehen 😞


  • Mod

    Tuxx schrieb:

    Deswegen ist das detach toll, damit es unabhängig im Hintergrund läuft.

    Das war ja auch der Plan 😃

    Die grundlegende Frage ist: Was soll passieren? Soll das Programm tendenziell eher warten, bis alle Subprozesse fertig sind, bevor es sich beendet? Oder sollen möglichst schnell alle Subprozesse beendet werden (auch wenn sie noch nicht fertig sind), wenn das Programm sich beenden möchte? Oder etwas dazwischen? Wenn diese Frage geklärt ist, kann man über die genaue Umsetzung reden.



  • Hallo SeppJ,

    da gebe ich dir vollkommen Recht 😃

    Ich habe mein Programm am laufen.
    Dieses erzeugt einen Server, der darauf wartet, dass sich ein Client verbindet.
    Ist dies geschehen, läuft mein eigentliches Programm, in dem ich zwei RepeatingTasks benötige.

    Wird der Client unverhofft gekappt oder das System will sich herunterfahren,
    muss das eigentliche Programm mit allen offenen Threads geschlossen werden, der Client eventuell geschlossen werden, der Server heruntergefahren werden und dann beendet sich auch meine main().

    Bis jetzt bekomme ich immer Fehler, dass auf Speicher noch zugegriffen wird, der nicht mehr existiert.
    Also muss ich irgendwie sicherstellen, dass das programm in dem Falle wartet, bis alle offenen Threads geschlossen wurden, bevor der Client und Server geschlossen wird.


  • Mod

    Wenn wir wirklich brav warten dürfen, bis alles fertig ist, können wir einfach mitzählen:

    #include <thread>
    #include <atomic>
    #include <chrono>
    #include <functional>
    
    class RepeatingTask
    {
    private:
      std::atomic_int running;
      std::function<void()> task;
      std::chrono::nanoseconds interval;
      std::atomic_uint num_running_threads;
    
      void execute_task()
      {
        while(running)
          {
            std::thread t([this]()
                          {
                            ++num_running_threads;
                            task();
                            --num_running_threads;
                          }
                          );
            t.detach();
            std::this_thread::sleep_for(interval);
          }
      }
    
    public:
      RepeatingTask(std::function<void()> task, std::chrono::nanoseconds interval):
        running(0), task(task), interval(interval), num_running_threads(0) {}
    
      RepeatingTask(const RepeatingTask&) = delete;
      RepeatingTask &operator=(const RepeatingTask&) = delete;
    
      void run()
      {
        running = 1;
        std::thread t(std::bind(&RepeatingTask::execute_task, this));
        t.detach();
      }
    
      void stop() {running = 0;}
      void join()
      {
        stop();
        while(num_running_threads)
          {
            std::this_thread::yield();
          }
      } 
    };
    
    // Beispiel
    #include <iostream>
    void ausgabe()
    {
      using namespace std::chrono_literals;
      for (int i = 1; i < 5; ++i)
        {
          std::cout << "Thread Nummer " << std::this_thread::get_id() << ", bei Zahl " << i << '\n';
          std::this_thread::sleep_for(1.4s);
        }
    }
    
    int main()
    {
      using namespace std::chrono_literals;
      RepeatingTask task(ausgabe, 1s);
      task.run();
      std::this_thread::sleep_for(5s);
      std::cout << "Leite Programmbeendigung ein.\n";
      task.join();
      std::cout << "Alle Hintergrundanwendungen sind fertig. Beende.\n";
    }
    


  • Danke, genau das, was ich gesucht habe 👍 👍 👍



  • Eine Frage noch an dich, SeppJ:

    Den Code über RepeatingTask hast du ja geschrieben oder ?

    Muss ich das in meinem Code irgendwie kennzeichnen zwecks copyright etc. ?


  • Mod

    Tuxx schrieb:

    Eine Frage noch an dich, SeppJ:

    Den Code über RepeatingTask hast du ja geschrieben oder ?

    Muss ich das in meinem Code irgendwie kennzeichnen zwecks copyright etc. ?

    Ja, das ist von mir. Aber die paar Zeilen, die ich da mal eben schnell runtergehackt habe, kannst du gerne übernehmen, das ist nicht schützenswert.



  • Wieso wird da eigentlich bei jedem Tick ein neuere Thread erzeugt, auch wenn die Funktion vom alten Tick noch nicht fertig gelaufen ist?
    Ist das wirklich Absicht?

    Ich hätte - wenn nicht speziell angegeben ist dass man es anders braucht - grundsätzlich mal die Version mit 1 Thread pro RepeatingTask implementiert. Also die wo execute_task die Task-Funktion synchron aufruft und dann wartet bis es Zeit für den nächsten Aufruf ist.



  • ps:
    Die RepeatingTask Objekte kann man nicht sauber löschen, da join() nicht sicherstellt dass execute_task() schon verlassen wurde.
    (Und natürlich sollte join vermutlich auch im Dtor aufgerufen werden, RRID und so.)


  • Mod

    hustbaer schrieb:

    Wieso wird da eigentlich bei jedem Tick ein neuere Thread erzeugt, auch wenn die Funktion vom alten Tick noch nicht fertig gelaufen ist?
    Ist das wirklich Absicht?

    Das war es, wie ich die Anforderungen des Threaderstellers in seinem Ursprungsthread verstanden habe. Ob der Threadersteller seine Anforderung auch wirklich richtig herüber gebracht hat, habe ich mich auch gefragt und daher absichtlich Beispiele konstruiert, die diese Eigenschaft demonstrieren. Er hat sich nicht beschwert.

    Falls man hingegen jeweils eine neue Ausführung der Aufgabe in einem bestimmten Abstand zur Beendigung der letzten Aufgabe haben möchte, wird das ganze Problem (sowohl das aktuelle, als auch die Ursprungsfrage) ziemlich trivial.

    hustbaer schrieb:

    ps:
    Die RepeatingTask Objekte kann man nicht sauber löschen, da join() nicht sicherstellt dass execute_task() schon verlassen wurde.
    (Und natürlich sollte join vermutlich auch im Dtor aufgerufen werden, RRID und so.)

    Guter Hinweis, habe ich übersehen. Hmm, trickreich. Da muss ich mal etwas darüber nachdenken. edit: Sollte gehen, indem man sich einen Handle auf den execute_task-Thread merkt und diesen in RepeatingTask::join selber joined. Oder übersehe ich da etwas?



  • Jo, ich frage auch hauptsächlich um dem Threadersteller das nochmal klarzumachen. Weil ich das im ursprünglichen Thread nicht so verstanden habe. Und wenn man es nicht so braucht, dann will man es vermutlich auch nicht so 🙂

    SeppJ schrieb:

    Falls man hingegen jeweils eine neue Ausführung der Aufgabe in einem bestimmten Abstand zur Beendigung der letzten Aufgabe haben möchte, wird das ganze Problem (sowohl das aktuelle, als auch die Ursprungsfrage) ziemlich trivial.

    Fixes Delay zwischen Ende des letzten Aufrufs und Start des neuen ist meist auch doof. Meist will man fixe Abstände zwischen den Anfängen der Aufrufe, ausser eben wenn der letzte Aufruf zu lange gedauert hat, dann den nächsten Aufruf einfach ohne Verzögerung. Wie bei Spielen mit VSync quasi 🙂

    Ist aber natürlich auch immer noch halbwegs einfach zu machen.

    hustbaer schrieb:

    Hmm, trickreich. Da muss ich mal etwas darüber nachdenken.

    Müsste ein join() auf den Thread der execute_task() ausführt reichen, oder?



  • SeppJ schrieb:

    edit: Sollte gehen, indem man sich einen Handle auf den execute_task-Thread merkt und diesen in RepeatingTask::join selber joined. Oder übersehe ich da etwas?

    Nö genau so hätte ich das auch gemacht.

    Wobei ich dann das sleep_for vermutlich durch ein std::condition_variable_any::wait_for ersetzen würde. Damit das joinen net unnötig verzögert wird. Dann muss natürlich ne mutex her, dafür braucht running nimmer atomic sein, weil man ja jetzt die mutex dafür hernehmen kann.

    Und wenn man dann eh schon ne CV + mutex hat, dann kann man auch gleich noch ne 2. CV für num_running_threads machen und den spin-wait durch nen CV-wait ersetzen.


  • Mod

    hustbaer schrieb:

    Wobei ich dann das sleep_for vermutlich durch ein std::condition_variable_any::wait_for ersetzen würde. Damit das joinen net unnötig verzögert wird. Dann muss natürlich ne mutex her, dafür braucht running nimmer atomic sein, weil man ja jetzt die mutex dafür hernehmen kann.

    Jupp, genau mein Gedanke.

    Und wenn man dann eh schon ne CV + mutex hat, dann kann man auch gleich noch ne 2. CV für num_running_threads machen und den spin-wait durch nen CV-wait ersetzen.

    Jetzt ist bloß noch die Frage, ob der TE mit der Beschreibung alleine zurecht kommt 🙂



  • Nö, aber dafür hat er ja den SeppJ 😃 🤡



  • Hallo Leute,

    erstmal danke SeppJ, dass ich den Code einfach so verwenden darf,
    mein Chef fragte nämlich, nicht dass unsere Firma da Probleme bekommt 🙂

    Auf die Idee mit mutex und CV bin ich auch schon gekommen gestern,
    nachdem mein Programm immer abstürzte wegen false memory access und abnormal interupt.

    Ich hab eigentlich genau das, was ihr in der letzten Nacht ;-), wo ich friedlich geschlafen habe, erörtert habt, gestern abend vor dem Feierabend noch umgesetzt.
    Ich hab nen mutex und eine CV und ein extra exec thread, der execute_task aufruft und einfach nur wartet und sicherstellt, dass alle offenen threads zum schluss geweckt und ordentlich beendet werden und erst dann beendet sich exec und mein Programm endet ohne Fehler 🙂

    Er überprüft einfach in der stop(), ob exec joinable ist und wenn ja, macht er den join() und wartet...

    Und wenn ich nicht gestern darauf gekommen wäre, hätte ich es jetzt gewusst durch eure gute Beschreibung - also Ja, ich wäre mit eurer Beschreibung zurecht gekommen

    😃 😃 😃


Anmelden zum Antworten