Einleitung: Warum werden Aufgabenwarteschlangen in C++ benötigt?
\n\nIn der modernen Programmierung, insbesondere bei der Arbeit mit Multithread-Anwendungen, entsteht häufig die Notwendigkeit, die Ausführung von Aufgaben in der Reihenfolge ihres Eingangs zu organisieren. Aufgabenwarteschlangen (Task Queues) sind ein grundlegendes Muster, das es ermöglicht, Arbeit effizient auf Threads zu verteilen, Anfragen von Webservern zu verarbeiten, Hintergrundprozesse zu verwalten und Race Conditions zu vermeiden.
\n\nIn C++ bietet die Standardbibliothek den leistungsstarken Container std::queue, der sich ideal für die Implementierung von Aufgabenwarteschlangen eignet. In der Praxis reicht es jedoch nicht aus, Elemente nur zu speichern; sie müssen sicher zwischen Threads übertragen, Fehler behandelt und der Lebenszyklus von Aufgaben verwaltet werden. In diesem Artikel analysieren wir, wie man Aufgabenwarteschlangen in C++ von Grund auf erstellt, unter Verwendung moderner Sprachfunktionen (C++11 und neuer).
Wir werden behandeln:
\n- \n
- Grundlegende Arbeit mit
std::queue\n - Threadsichere Aufgabenwarteschlange \n
- Implementierung eines Thread Pools \n
- Prioritätswarteschlangen \n
1. Grundlegende Aufgabenwarteschlange mit std::queue
\n\nBeginnen wir mit einem einfachen Beispiel. std::queue ist ein Container-Adapter, der nach dem FIFO-Prinzip (First In, First Out) funktioniert. Er stellt die Methoden push(), pop(), front() und empty() bereit.
So kann eine Aufgabenwarteschlange organisiert werden, bei der jede Aufgabe eine Funktion (oder ein aufrufbares Objekt) ist:
\n\n#include <iostream>\n#include <queue>\n#include <functional>\n\nint main() {\n // Aufgabenwarteschlange, bei der jede Aufgabe ein std::function<void()> ist\n std::queue<std::function<void()>> taskQueue;\n\n // Aufgaben hinzufügen\n taskQueue.push([]() { std::cout << "Aufgabe 1 ausgeführt\\n"; });\n taskQueue.push([]() { std::cout << "Aufgabe 2 ausgeführt\\n"; });\n taskQueue.push([]() { std::cout << "Aufgabe 3 ausgeführt\\n"; });\n\n // Warteschlange verarbeiten\n while (!taskQueue.empty()) {\n auto task = taskQueue.front();\n task(); // Aufgabe ausführen\n taskQueue.pop(); // Ausgeführte Aufgabe entfernen\n }\n\n return 0;\n}\n\nDieser Code funktioniert, jedoch nur im Singlethread-Modus. Wenn mehrere Threads gleichzeitig Aufgaben hinzufügen oder entnehmen, entsteht eine Race Condition. Für Multithreading ist eine Synchronisation erforderlich.
\n\n2. Threadsichere Aufgabenwarteschlange
\n\nUm die Warteschlange für den Multithread-Zugriff sicher zu machen, verwenden wir einen Mutex (std::mutex) und eine Bedingungsvariable (std::condition_variable). Dies ist das klassische Producer-Consumer-Muster (Erzeuger-Verbraucher).
Implementierung einer threadsicheren Warteschlange:
\n\n#include <queue>\n#include <mutex>\n#include <condition_variable>\n#include <functional>\n#include <thread>\n\nclass ThreadSafeQueue {\nprivate:\n std::queue<std::function<void()>> queue_;\n std::mutex mutex_;\n std::condition_variable cond_;\n bool stop_ = false;\n\npublic:\n void push(std::function<void()> task) {\n std::lock_guard<std::mutex> lock(mutex_);\n queue_.push(task);\n cond_.notify_one(); // Einen wartenden Thread benachrichtigen\n }\n\n std::function<void()> pop() {\n std::unique_lock<std::mutex> lock(mutex_);\n cond_.wait(lock, [this]() { return !queue_.empty() || stop_; });\n if (stop_ && queue_.empty()) {\n return nullptr; // Stoppsignal\n }\n auto task = queue_.front();\n queue_.pop();\n return task;\n }\n\n void stop() {\n std::lock_guard<std::mutex> lock(mutex_);\n stop_ = true;\n cond_.notify_all(); // Alle Threads zum Beenden aufwecken\n }\n};\n\nWichtige Punkte:
\n- \n
std::lock_gu