So implementieren Sie einen Thread-Pool mit C++ (C++11-Standard)

Vorne geschrieben: Dieser Artikel stammt aus dem CSDN-Blog: https://blog.csdn.net/xyygudu/article/details/128767928 , Autor: xyygudu

Es gibt nicht viel zu sagen, setzen Sie den Code an die erste Stelle

https://github.com/xyygudu/ThreadPool

Sind Multithread-Programme unbedingt gut?

Nicht unbedingt, es hängt von der Art des aktuellen Programms ab. Zu den Programmtypen gehören E/A-intensiv und CPU-intensiv.

  • E/A-intensiv: Anweisungen, die einige E/A-Vorgänge umfassen, z. B. Geräte, Dateien, Netzwerke usw. Diese E/A-Vorgänge können leicht von Programmen blockiert werden und sind zeitaufwändige Vorgänge.

  • CPU-intensiv: Anweisungen werden hauptsächlich für Berechnungen verwendet.

Für Multi-Core-Computer ist es erforderlich, dass diese beiden Arten von Programmen Multithreading verwenden. Wenn es sich jedoch um eine Single-Threaded-Umgebung handelt, eignen sich IO-intensive Programme für Multithreading . Wenn beispielsweise eine Aufgabe ausgeführt wird In einem Netzwerkblockierungszustand kann umgeschaltet werden. Andere Threads werden zur Verarbeitung anderer Aufgaben verwendet. CPU-intensive Programme sind nicht für Multithreading geeignet, da diese Berechnungsanweisungen auch bei mehreren Threads weiterhin von diesem Kern verarbeitet werden. Darüber hinaus Beim Ausführen von Anweisungen gibt es auch einen gewissen Overhead beim Threadwechsel.

Wie bestimme ich die Anzahl der Threads?

  • Das Erstellen und Zerstören von Threads ist ein sehr „schwerer“ Vorgang.

  • Threads selbst beanspruchen viel Speicher. Jeder Thread verfügt über seinen eigenen unabhängigen Stapelspeicher.

  • Der Kontextwechsel von Threads ist zeitaufwändig. Wenn die CPU viel Zeit damit verbringt, Threads zu wechseln, hat sie keine Zeit, Aufgaben zu verarbeiten.

  • Das gleichzeitige Aufwachen einer großen Anzahl von Threads führt zu einer übermäßigen momentanen Belastung des Systems und zu Ausfallzeiten.

Die Anzahl der Threads richtet sich im Allgemeinen nach der Anzahl der Kerne der aktuellen CPU.

Einführung in den Thread-Pool

Grundlagen des Thread-Pools

Damit Aufgaben zeitnah abgearbeitet werden können (die sogenannten Aufgaben können als auszuführende Funktionen verstanden werden), werden alle ausstehenden Aufgaben in die Aufgabenwarteschlange des Thread-Pools gestellt, und mehrere Threads im Thread-Pool können annehmen Aufgaben aus der Aufgabenwarteschlange herausnehmen und ausführen. Während der Thread die Aufgabe herausnimmt, kann der Benutzer auch eine neue Aufgabe zur Aufgabenwarteschlange hinzufügen, als ob der Benutzer dem Thread-Pool nur die auszuführende Aufgabe mitteilen müsste, und Der Thread-Pool gibt das Benutzerverarbeitungsergebnis zurück, nachdem die interne Thread-Verarbeitung abgeschlossen ist. Das allgemeine Prinzip ist in der folgenden Abbildung dargestellt.
Fügen Sie hier eine Bildbeschreibung ein
Einige Leser fragen sich vielleicht: Wird die Funktion nach dem Aufruf nicht ausgeführt? Wie kann sie in eine Aufgabenwarteschlange gestellt werden? Ja, C ++ 11 bietet eine Funktionsvorlage, mit der die Funktion als aufrufbares Objekt (als Variable verständlich) oder sogar in einem Container (z. B. einer Warteschlange) gespeichert werden kann Das Aufrufobjekt kann sein zu gegebener Zeit herausgenommen und dann ausgeführt werden. Dies entspricht dem Speichern der auszuführenden Funktion und dem späteren Aufruf.

Vorteile des Thread-Pools

  • Zu Beginn des Service-Prozessstarts wurden viele Threads im Thread-Pool im Voraus neu erstellt. Wenn eine Aufgabe ausgeführt werden muss, können Sie direkt einen Thread auswählen, um die Aufgabe sofort auszuführen, anstatt zusätzliche Zeit dafür zu benötigen Erstellen Sie die Aufgabe wie zuvor. Thread.

  • Nach Abschluss der Aufgabenausführung ist es nicht erforderlich, den Thread freizugeben, sondern ihn an den Thread-Pool zurückzugeben, um Dienste für nachfolgende Aufgaben bereitzustellen.

Die Funktionen, die der Thread-Pool haben sollte

  • Der Thread-Pool sollte konfigurierbar sein. Konfigurieren Sie beispielsweise die Anzahl der reservierten Threads, die maximale Anzahl der Threads, die vom Thread-Pool geöffnet werden können usw.

  • Verfügt über reservierte Threads und dynamische Threads. Der reservierte Thread befindet sich im Thread-Pool. Solange der Thread-Pool nicht beendet oder recycelt wird, wird der residente Thread nicht beendet. Der dynamische Thread wird entsprechend der Größe der Aufgabe dynamisch erstellt und zerstört. Wenn der reservierte Thread Die Aufgaben in der Aufgabenwarteschlange können nicht rechtzeitig verarbeitet werden. Erhöhen Sie den dynamischen Thread. Wenn die Aufgabenwarteschlange leer ist und sich viele dynamische Threads im Wartezustand befinden, beenden Sie den dynamischen Thread.

  • Kann verschiedene Aufgaben ausführen (Funktionen mit unterschiedlichen Parametern) und Ausführungsergebnisse erhalten

  • Die Gesamtzahl der Threads im aktuellen Thread-Pool und die Anzahl der inaktiven Threads können abgerufen werden.

  • Die Anzahl der Aufgaben in der Aufgabenwarteschlange kann gesteuert werden, um zu verhindern, dass zu viele Aufgaben viel Speicher belegen.

  • Der Schalter zum Aktivieren der Thread-Pool-Funktion. Stellen Sie beim Schließen sicher, dass die aktuelle Aufgabe ausgeführt werden kann, und verwerfen Sie die verbleibenden nicht ausgeführten Aufgaben oder beenden Sie das Hinzufügen neuer Aufgaben zur Aufgabenwarteschlange und beenden Sie den Vorgang, nachdem die verbleibenden Aufgaben ausgeführt wurden.

Implementierung des Thread-Pools

Vorkenntnisse

Dieser Thread-Pool ist in C++11 geschrieben und verwendet hauptsächlich die folgenden C++11-Funktionen, was für Anfänger geeignet ist.

std::functionund std::bind, verschiedene Funktionsvorlagen, package_task und Future, atomare Typen (atomar), Bedingungsvariablen, Mutexe, intelligente Zeiger, Lambda-Ausdrücke, Bewegungssemantik, perfekte Weiterleitung

Klasseneinteilung

Der Thread-Pool ist in zwei Klassen unterteilt, eine Thread-Klasse und eine ThreadPool-Klasse. Die Thread-Klasse wird hauptsächlich zum Speichern einiger grundlegender Attribute des Threads verwendet, z. B. des Thread-Flags (ob es sich um einen reservierten Thread oder einen dynamischen Thread handelt). Thread-ID usw., std::threadeinschließlich Die Methode dient hauptsächlich dem Starten des Threads (Startfunktion); die ThreadPool-Klasse wird hauptsächlich zum Verwalten der Thread-Klasse und der Aufgabenwarteschlange verwendet, einschließlich des dynamischen Hinzufügens/Löschens von Threads und des Zugreifens auf Aufgaben zur Aufgabenwarteschlange. usw. Der Hauptcode der Header-Dateien dieser beiden Klassen lautet wie folgt:

class Thread : public std::enable_shared_from_this<Thread>
{
    
    
 
public:
    using ThreadPtr = std::shared_ptr<Thread>;
    using ThreadFunc = std::function<void(ThreadPtr)>; 
    enum class ThreadFlag {
    
     kReserved, kDynamic };
 
    explicit Thread(ThreadFunc func, ThreadFlag flag = ThreadFlag::kReserved);
    ~Thread() = default;
    // 获取线程id
    pid_t getThreadId() {
    
     return threadId_; }  
    void start();               // 启动线程
 
private:
    ThreadFlag threadFlag_;     // 线程标志
    pid_t threadId_;            // 线程id
    std::thread thread_;        // 线程
    ThreadFunc threadFunc_;     // 线程执行函数
};

class ThreadPool
{
    
    
public:
    using Task = std::function<void()>;                     // 定义任务回调函数
    using Seconds = std::chrono::seconds;
    using ThreadPoolLock = std::unique_lock<std::mutex>;
    using ThreadPtr = std::shared_ptr<Thread>;              // 线程指针类型
    using ThreadFlag = Thread::ThreadFlag;
 
    explicit ThreadPool();
    ~ThreadPool();
 
    void start();                                           // 开启线程池
    void stop();                                            // 停止线程池
    template<typename Func, typename... Args>               // 给线程池提交任务
    auto submitTask(Func&& func, Args&&... args) -> std::future<decltype(func(args...))>;
 
private:
    void addThread(ThreadFlag flag = ThreadFlag::kReserved);// 向线程池中添加一个新线程
    void removeThread(pid_t id);
    void threadFunc(ThreadPtr threadPtr);                   // 线程执行函数
 
    std::list<ThreadPtr> workThreads_;                      // 存放工作线程,使用智能指针是为了能够自动释放Thread
    std::mutex threadMutex_;                                // 工作线程互斥锁
    std::queue<Task> tasks_;                                // 任务队列
    std::mutex taskMutex_;                                  // 互斥锁(从队列取出/添加任务用到)
    std::condition_variable notFullCV_;                     // 任务队列不满
    std::condition_variable notEmptyCV_;                    // 任务队列不空
    std::condition_variable exitCV_;                        // 没有任务时方可线程池退出
 
    // std::atomic_int taskNum_;                            // 任务队列中未处理的任务数量
    std::atomic_int waitingThreadNum_;                      // 处于等待中的线程数量
    std::atomic_uint curThreadNum_;                         // 当前线程数量
 
    std::atomic_bool quit_;                                 // 标识是否退出线程池
 
    Config config_;                                         // 存储线程池配置
};

Funktionsrealisierung

Was macht der Header-ThreadPool-Konstruktor?

Der Konstruktor initialisiert hauptsächlich einige Konfigurationen, z. B. die Anzahl der reservierten Threads, die maximale Anzahl von Threads im Thread-Pool usw.

Was macht der Thread-Pool, wenn er startet (startet)?
Nachdem der Thread-Pool die Startfunktion aufgerufen hat, fügt er reservierte Threads zum Thread-Container workThreads_ entsprechend der in der Konfiguration angegebenen Anzahl reservierter Threads hinzu. Durch das Hinzufügen reservierter Threads werden tatsächlich Thread-Objekte erstellt und diese Threads gleichzeitig gestartet. Der Thread Die Klasse verfügt auch über eine Startfunktion. Die Funktion erstellt formal ein std::thread-Objekt und bindet die auszuführende Rückruffunktion (dh die threadFunc-Funktion der ThreadPool-Klasse) an std::thread. Danach ist der Thread-Pool Gestartet, alle reservierten Threads befinden sich zu diesem Zeitpunkt im laufenden Zustand und warten auf das Eintreffen der Aufgabe. Warum die Thread-Klasse mit einem Smart Pointer gekapselt werden sollte, erfahren Sie in der Antwort „Warum einen Smart Pointer für die Thread-Klasse verwenden“.

void ThreadPool::start()
{
    
    
    quit_ = false;
    size_t reservedThreadNum = config_.RESERVED_THREAD_NUM;
    std::cout << "init thread num: " << reservedThreadNum << std::endl;
    while (reservedThreadNum--)
    {
    
    
        addThread(); // 默认创建保留线程(threadFlag_=kReserved)
    }
}
 
void ThreadPool::addThread(ThreadFlag flag)
{
    
    
    // 默认创建保留线程(threadFlag_=kReserved)
    ThreadPtr newThreadPtr(new Thread(std::bind(&ThreadPool::threadFunc, this, std::placeholders::_1), flag));
    newThreadPtr->start();                  // 启动线程,线程自动执行threadFunc
    // 如果不把newThreadPtr添加到workThread_,函数运行结束后,newThreadPtr的引用计数为0,资源会被释放
    ThreadPoolLock lock(threadMutex_);
    workThreads_.emplace_back(newThreadPtr);
    curThreadNum_++;
}

Wie gehen Unterthreads mit Aufgaben um?

Was der Sub-Thread ausführt, ist eine Endlosschleife, nur wenn das Exit-Flag des beendenden Threads quit_=truedie Schleife verlässt und der Sub-Thread beendet ist. Der Unterthread wird in der threadFunc-Funktion von ThreadPool ausgeführt. Die Arbeitsschritte dieser Funktion sind wie folgt:

  1. Erwerben Sie einen Mutex. Da mehrere Threads Aufgaben aus der Aufgabenwarteschlange entfernen müssen, ist die Aufgabenwarteschlange „tasks_“ eine kritische Ressource und untergeordnete Threads müssen gesperrt werden, um auf kritische Ressourcen zugreifen zu können.
  2. Bestimmen Sie anhand der Bedingungsvariablen notEmptyCV_, ob die Aufgabenwarteschlange „tasks_“ leer ist. Nur wenn „tasks_“ leer ist und der Thread-Pool das Exit-Flag gesetzt hat (dh quit_=true), wird der untergeordnete Thread selbst aus „workThreads_“ des Thread-Pools entfernt. Der Zweck besteht darin, den Thread-Pool weiter ausführen zu lassen, solange die Aufgabenwarteschlange nicht leer ist, obwohl das Exit-Flag gesetzt ist. Mit anderen Worten soll verhindert werden, dass der Thread-Pool Aufgaben verlässt, die nicht verarbeitet wurden. In anderen Fällen werden Aufgaben aus der Aufgabenwarteschlange übernommen und ausgeführt.

Wenn der untergeordnete Thread die Exit-Bedingung erfüllt, quit_ && tasks_.empty()ist dies wahr. Zusätzlich zum Entfernen aus dem Thread-Pool gibt es auch eine Benachrichtigungsbedingungsvariable exitCV_. Der Zweck besteht darin, alle Threads aus dem Thread-Pool zu entfernen und dann den Thread-Pool zu zerstören. Weitere Informationen zum Thema „Objekt“ finden Sie unter „So verlassen Sie den Thread-Pool sicher?“. Der Hauptcode lautet wie folgt (nehmen Sie den reservierten Thread als Beispiel, der dynamische Thread ist nur ein weiteres Timeout-Recycling):

void ThreadPool::threadFunc(ThreadPtr threadPtr)
{
    
    
    for(;;)
    {
    
    
        Task task;
        {
    
    
            ThreadPoolLock lock(taskMutex_);
            std::cout << "tid:" << threadPtr->getThreadId() << " try to get one task..." << std::endl;
            waitingThreadNum_++;
            // 只要任务队列不为空或者线程池要退出,就不阻塞在wait
            if (threadPtr->getThreadFlag() == Thread::ThreadFlag::kReserved)
            {
    
    
                notEmptyCV_.wait(lock, [this]() {
    
     return quit_ || !tasks_.empty(); });
                waitingThreadNum_--;
                // 只有当线程池quit_=true并且没有任务要处理的情况下,才会真正退出线程池
                if (quit_ && tasks_.empty())
                {
    
    
                    removeThread(threadPtr->getThreadId());
                    exitCV_.notify_all();
                    std::cout << "Reserved thread<" << threadPtr->getThreadId() << "> exit" << std::endl;
                    return;
                }
                else{
    
    
                    task = std::move(tasks_.front());
                    tasks_.pop();
                    notFullCV_.notify_all();
                }
            }
            else
            {
    
    
                // 动态线程处理过程
            } 
        }
        if (task != nullptr)  task();                    // 执行任务
        else std::cout << "task is null" << std::endl;
       
        std::cout << "thread(id: " << threadPtr->getThreadId() << ") is running task" << std::endl;
    }
}

Wie übermittle ich Aufgaben an den Thread-Pool?

Beim Senden einer Aufgabe wird die Aufgabe gepackt und zur Aufgabenwarteschlange hinzugefügt. Der spezifische Prozess ist wie folgt:

  1. Packen Sie die Aufgabe, dh packen Sie die vom Benutzer übermittelte Aufgabe in ein aufrufbares Objekt, um sie der Aufgabenwarteschlange hinzuzufügen. Die Funktion „submitTask“ verwendet eine Vorlage für variable Parameter. Der Zweck besteht darin, verschiedene vom Benutzer übergebene Aufgaben verarbeiten zu können (dh Funktionen verarbeiten zu können, deren Parameter nicht festgelegt sind).
  2. Fügen Sie Aufgaben zur Aufgabenwarteschlange hinzu. Erwerben Sie zunächst die Sperre, bevor Sie die gepackte Aufgabe zur Aufgabenwarteschlange task_ hinzufügen, und beurteilen Sie dann mithilfe der Bedingungsvariablen notFullCV_, ob die Aufgabe in der Aufgabenwarteschlange die Obergrenze erreicht hat. Wenn sie diese erreicht hat, wird hier bis zum Die Aufgabenwarteschlange erfüllt die Bedingungen (hier scheint ein kleiner Fehler vorzuliegen: Wenn der Thread-Pool das Exit-Flag setzt, sollte er keine weiteren Aufgaben zur Aufgabenwarteschlange hinzufügen, aber es gibt hier keine Beurteilung, ob der Thread-Pool beendet wird Obwohl der Thread-Pool Das Exit-Flag gesetzt ist, kann der Thread-Pool nicht beendet werden, solange noch Aufgaben zur Aufgabenwarteschlange hinzugefügt werden, da die einzige Bedingung für das erfolgreiche Beenden des Thread-Pools ist) und dann notEmptyCV_ quit_=true && tasks_.empty()=truebenachrichtigt Andere Threads weisen darauf hin, dass die Aufgabenwarteschlange nicht leer ist.
  3. Fügen Sie dynamische Threads hinzu. Solange die Anzahl der wartenden Threads geringer ist als die Anzahl der abzuarbeitenden Aufgaben, werden neue dynamische Threads erstellt.
template <typename Func, typename... Args>
inline auto ThreadPool::submitTask(Func &&func, Args &&...args) -> std::future<decltype(func(args...))>
{
    
    
    using ReturnType = decltype(func(args...));
    auto task = std::make_shared<std::packaged_task<ReturnType()>>(std::bind(std::forward<Func>(func), std::forward<Args>(args)...));
    std::future<ReturnType> result = task->get_future();
    size_t taskSize = 0;
    {
    
      
        // 获取锁
        ThreadPoolLock lock(taskMutex_);
        notFullCV_.wait(lock, [this]()->bool {
    
     
            bool notfull = false;
            if (tasks_.size() < (size_t)config_.MAX_TASK_NUM) notfull = true;
            else std::cout << "task queue is full..." << std::endl;
            return notfull;
        });
        // 如果任务队列不满,则把任务添加进队列
        tasks_.emplace([task]() {
    
    (*task)();});
        taskSize = tasks_.size();
        notEmptyCV_.notify_one();
    }
 
 
    // 根据任务队列中任务的数量以及空闲线程数量决定要不要新增线程
    if ((size_t)waitingThreadNum_ < taskSize && curThreadNum_ < config_.MAX_THREAD_NUM)
    {
    
    
        addThread(ThreadFlag::kDynamic);
    }
    return result;
}

Wie kann ich den Thread-Pool sicher verlassen?

Wenn der Thread-Pool beendet wird, ist es am besten, zu warten, bis alle untergeordneten Threads beendet sind, bevor er sich selbst beendet. Dies liegt daran, dass der Sub-Thread die Ressourcen des Haupt-Threads (Thread-Pools) verwendet, z. B. durch Ändern der Variablen waitingThreadNum_ usw. Wenn das Thread-Pool-Objekt zerstört wurde und der Sub-Thread noch ausgeführt wird, sind die Folgen unvorstellbar Schauen wir uns an dieser Stelle an, was die Stoppfunktion bewirkt.

  1. Setzen Sie zuerst quit_ auf true. Dieser Schritt ist sehr wichtig. Die untergeordneten Threads überprüfen das quit_-Flag in der Endlosschleife. quit_=trueZu diesem Zeitpunkt führen sie die entsprechende Exit-Verarbeitung selbst durch.
  2. Dann benachrichtigt notEmptyCV_ alle untergeordneten Threads. Dies liegt daran, dass sich einige Threads im Wartezustand befinden, weil die Aufgabenwarteschlange leer ist und nicht weiter ausgeführt werden kann. Daher ist es nicht wahr, dass die Aufgabenwarteschlange nicht leer ist, sondern notEmptyCV_.notify_all()das wartende Kind aufzuwecken Threads. , damit sie normal beendet werden.
  3. Warten Sie, bis sich alle untergeordneten Threads aus dem Thread-Pool entfernt haben. Nachdem jeder Sub-Thread die Endbedingung erfüllt hat, wird er aus workThreads_ entfernt. Erst workThreads_.size()==0dann kann gesagt werden, dass alle Sub-Threads beendet wurden. Zu diesem Zeitpunkt, nachdem sich der letzte Sub-Thread aus workThreads_ entfernt hat, wird er dies tun Durch exitCV_.notify_all()Sagen Der Thread-Pool muss nicht warten, da die Größe von workThreads_ zu diesem Zeitpunkt bereits 0 ist und der Thread-Pool erfolgreich beendet wird.

Einige Leser haben möglicherweise Fragen: Kann es nicht ausreichen, join() für jeden Unterthread in workThreads_ aufzurufen? Warum die Bedingungsvariable exitCV_ verwenden? Die Antwort finden Sie unter „Wie werden Unterthreads recycelt“.

void ThreadPool::stop()
{
    
    
    // 线程池退出时,线程只有两种状态,要么wait要么run,所以要把处于wait状态的线程唤醒
    quit_ = true;
    // 唤醒所有处于kWaiting的线程
    ThreadPoolLock lock(taskMutex_);
    notEmptyCV_.notify_all();     
    // 等待所有线程退出并移出所有线程
    exitCV_.wait(lock, [&]()->bool {
    
     return workThreads_.size() == 0; });
    std::cout << "all thread remove from workThreads_ successfully" << std::endl;  
}

FAQ

Wann werden dynamische Threads gestartet?

Es erscheint unangemessen, wenn die Aufgabenwarteschlange einen dynamischen Thread startet, solange Aufgaben vorhanden sind, da der reservierte Thread wahrscheinlich noch erstellt wird. Zu diesem Zeitpunkt befinden sich möglicherweise mehrere Aufgaben in der Aufgabenwarteschlange, die dies nicht können verarbeitet werden. Wenn der reservierte Thread erstellt wird, reicht es wahrscheinlich aus, die verbleibenden Aufgaben in der Aufgabenwarteschlange zu verarbeiten. Wie viele Aufgaben befinden sich also in der Aufgabenwarteschlange, um einen dynamischen Thread zu starten? Antwort: Sie können die Anzahl der Aufgaben in der Aufgabenwarteschlange mit der Anzahl der inaktiven Unterthreads vergleichen. Wenn die Anzahl der wartenden Threads geringer ist als die Anzahl in der Aufgabenwarteschlange, können Sie dynamische Threads erstellen, aber es scheint, dass dies der Fall ist Außerdem: Reservierte Threads werden noch erstellt. Zu diesem Zeitpunkt ist die Anzahl der Aufgabenwarteschlangen größer als die Anzahl der bereits wartenden Threads, was zur Erstellung unnötiger dynamischer Threads führt. Das Problem scheint jedoch nicht groß zu sein. Dies dient nur dazu, den Leser zum Nachdenken anzuregen.

Wie werden untergeordnete Threads recycelt?

Wann werden die Ressourcen des Threads zurückgefordert (verbinden oder trennen) und wann wird die Klasse Thread, zu der der Thread gehört, freigegeben, nachdem die Ausführung der vom untergeordneten Thread ausgeführten Funktion beendet ist (die Schleife verlassen)? Antwort: Detach wird aufgerufen, um den Thread zu trennen, wenn der Thread gestartet wird. Obwohl Detach sehr vorsichtig verwendet werden sollte, gibt es hier keine Möglichkeit. Die Startfunktion kann Join nicht aufrufen, da der Hauptthread in der Join-Funktion blockiert wird, wenn das untergeordnete Element vorhanden ist Der Thread wird nicht beendet, was dazu führt, dass der Start fehlschlägt. Wenn die Ausführung nicht beendet werden kann, senden Sie eine schwierige Aufgabe. Können Sie Join dann nicht in anderen Funktionen aufrufen, z. B. indem Sie die Sub-Threads in workThreads_ nacheinander in der Stop-Funktion durchlaufen, um beizutreten? Nein, da es sich um dynamische Threads handelt, die recycelt werden müssen. Wenn ein Sub-Thread auf eine Zeitüberschreitung wartet und recycelt werden muss, entfernt sich der Sub-Thread selbst aus workThreads_, und wenn der nachfolgende Thread-Pool workThreads_ durchquert, ist dies nicht möglich Um den neu entfernten Unterthread zu finden, kann der Thread daher nicht aus workThreads_ herausgenommen werden, und für seinen Thread wird Join aufgerufen. Daher ruft der in der Startfunktion in diesem Artikel erstellte Thread „detach“ auf, um den Thread zu recyceln. Einige Leser haben eine weitere Frage: Kann der untergeordnete Thread beitreten oder sich trennen, wenn er sich von workThreads_ entfernt? Nein, da Sub-Threads sich nicht selbst verbinden oder trennen können, sonst tritt ein Fehler auf)

Warum verwendet die Thread-Klasse intelligente Zeiger?

Antwort: Der Thread muss zum Container des Thread-Pools hinzugefügt werden. Im Vergleich zum Hinzufügen der Thread-Klasse zum Container ist das Hinzufügen eines Zeigers offensichtlich nicht effizient. Wenn es sich jedoch um den hinzugefügten nackten Zeiger Thread* handelt, ist dies erforderlich Manuell neues Thread-ObjektWenn Sie das Löschen vergessen, führt dies zu Speicherverlusten. Verwenden Sie daher intelligente Zeiger, um Threads zu umschließen, sowohl gemeinsam genutzte Zeiger als auch exklusive Zeiger

Warum sollten alle untergeordneten Threads beendet werden, bevor der Thread-Pool zerstört wird?

Da der untergeordnete Thread die Ressourcen des Hauptthreads (Thread-Pool) (z. B. die Variable quit_) verwendet, ist es wahrscheinlich, dass der untergeordnete Thread noch läuft, wenn der Thread-Pool zuerst zerstört wird, d wurde zerstört, was dazu führte, dass der untergeordnete Thread aufgrund einer Ausnahme beendet wurde. Deshalb empfehle ich, die Fäden vorsichtig zusammenzufügen und zu lösen.

Wie kann der Hauptthread beendet werden, nachdem alle Threads beendet wurden?

  • Methode 1: Führen Sie eine neue bedingte Variable exitCV_ ein. Das heißt, die Methode, die ich verwende: Verwenden Sie einen exitCV_ in der Stoppfunktion des Thread-Pools, um zu prüfen, ob worThreads_ leer ist, und warten Sie, ob er nicht leer ist. Dies erfordert, dass jeder Unterthread direkt gesetzt wird Entfernen Sie sich von workThreads_ und at Gleichzeitig wird exitCV_.notify_all(); zu diesem Zeitpunkt, obwohl der exitCV_ des Sub-Threads benachrichtigt wurde, solange worThreads_ nicht leer ist, immer noch in einem Wartezustand sein, bis sich alle Sub-Threads selbst in den Wartezustand versetzen von workThreads_.
  • Methode 2: Fügen Sie für jede Thread-Klasse ein Exit-Flag hinzu, vorausgesetzt, es handelt sich um kStop. Diese Methode entfernt sich nicht selbst aus workThreads_, wenn der untergeordnete Thread beendet wird, sondern markiert sich nur als kStop-Status, um die Ereignisschleife zu verlassen. Wenn Sie beitreten oder trennen möchten, d. h. wenn der Thread-Pool beendet wird (Aufruf stop), durchlaufen Sie zuerst workThreads_ und verbinden Sie sich oder trennen Sie jeden Thread und löschen Sie dann workThreads_. Vorteile: Die Bedingungsvariable exitCV_ muss nicht eingeführt werden, und der Unterthread kann durch Beitritt beendet werden, was sicherer ist. Nachteil: Der Unterthread wird beendet, aber die Thread-Klasse, zu der der Unterthread gehört, wird nicht freigegeben Dies führt zu komplizierteren Problemen beim anschließenden dynamischen Hinzufügen von Unter-Threads. Situation: Die Threads in workThreads_, die sich im Stoppstatus befinden, sollten zuerst aktiviert werden. Wenn Sie noch Threads hinzufügen müssen, erstellen Sie neue Threads.

Supongo que te gusta

Origin blog.csdn.net/hallobike/article/details/130260859
Recomendado
Clasificación