Pool di thread in C ++ 11


131

Domande rilevanti :

Informazioni su C ++ 11:

Informazioni su Boost:


Come posso ottenere un pool di thread a cui inviare attività senza crearle ed eliminarle più e più volte? Ciò significa che i thread persistenti si risincronizzano senza unirsi.


Ho un codice simile al seguente:

namespace {
  std::vector<std::thread> workers;

  int total = 4;
  int arr[4] = {0};

  void each_thread_does(int i) {
    arr[i] += 2;
  }
}

int main(int argc, char *argv[]) {
  for (int i = 0; i < 8; ++i) { // for 8 iterations,
    for (int j = 0; j < 4; ++j) {
      workers.push_back(std::thread(each_thread_does, j));
    }
    for (std::thread &t: workers) {
      if (t.joinable()) {
        t.join();
      }
    }
    arr[4] = std::min_element(arr, arr+4);
  }
  return 0;
}

Invece di creare e unire i thread ogni iterazione, preferirei inviare attività ai miei thread di lavoro ogni iterazione e crearli una sola volta.


1
ecco una domanda correlata e la mia risposta.
didierc,

1
hai pensato di usare tbb (è Intel, ma gratuito e open source, e fa esattamente quello che vuoi: devi semplicemente inviare attività (divisibili in modo ricorsivo) e non preoccuparti dei thread)?
Walter,

2
Questo progetto FOSS è il mio tentativo di creare una libreria di pool di thread, verificalo se vuoi. -> code.google.com/p/threadpool11
Etherealone

Cosa c'è di sbagliato nell'usare tbb?
Walter,

Risposte:


84

Puoi utilizzare la libreria del pool di thread C ++, https://github.com/vit-vit/ctpl .

Quindi il codice che hai scritto può essere sostituito con il seguente

#include <ctpl.h>  // or <ctpl_stl.h> if ou do not have Boost library

int main (int argc, char *argv[]) {
    ctpl::thread_pool p(2 /* two threads in the pool */);
    int arr[4] = {0};
    std::vector<std::future<void>> results(4);
    for (int i = 0; i < 8; ++i) { // for 8 iterations,
        for (int j = 0; j < 4; ++j) {
            results[j] = p.push([&arr, j](int){ arr[j] +=2; });
        }
        for (int j = 0; j < 4; ++j) {
            results[j].get();
        }
        arr[4] = std::min_element(arr, arr + 4);
    }
}

Otterrai il numero desiderato di thread e non li creerai ed eliminerai più e più volte nelle iterazioni.


11
Questa dovrebbe essere la risposta; libreria C ++ 11 a intestazione singola, leggibile, semplice, concisa e conforme agli standard. Ottimo lavoro!
Jonathan H,

@ vit-vit puoi fare un esempio con una funzione per favore? come si spinge una funzione membro della classe aresults[j] = p.push([&arr, j](int){ arr[j] +=2; });
Hani Goc

1
@HaniGoc Basta catturare l'istanza per riferimento.
Jonathan H,

@ vit-vit Ti ha inviato una richiesta pull per migliorare la versione STL.
Jonathan H,

@ vit-vit: è difficile contattare il manutentore di quella biblioteca con domande, suggerimenti e suggerimenti.
einpoklum,

83

Questo è copiato dalla mia risposta a un altro post molto simile, spero che possa aiutare:

1) Inizia con il numero massimo di thread che un sistema può supportare:

int Num_Threads =  thread::hardware_concurrency();

2) Per un'implementazione efficiente di threadpool, una volta creati i thread in base a Num_Threads, è meglio non crearne di nuovi o distruggere quelli vecchi (unendosi). Ci sarà una penalità per le prestazioni, potrebbe persino rallentare l'applicazione rispetto alla versione seriale.

Ogni thread C ++ 11 dovrebbe essere in esecuzione nella loro funzione con un ciclo infinito, in costante attesa di nuove attività da afferrare ed eseguire.

Ecco come associare tale funzione al pool di thread:

int Num_Threads = thread::hardware_concurrency();
vector<thread> Pool;
for(int ii = 0; ii < Num_Threads; ii++)
{  Pool.push_back(thread(Infinite_loop_function));}

3) La funzione Infinite_loop_

Questo è un ciclo "while (true)" in attesa della coda delle attività

void The_Pool:: Infinite_loop_function()
{
    while(true)
    {
        {
            unique_lock<mutex> lock(Queue_Mutex);

            condition.wait(lock, []{return !Queue.empty() || terminate_pool});
            Job = Queue.front();
            Queue.pop();
        }
        Job(); // function<void()> type
    }
};

4) Crea una funzione per aggiungere lavoro alla tua coda

void The_Pool:: Add_Job(function<void()> New_Job)
{
    {
        unique_lock<mutex> lock(Queue_Mutex);
        Queue.push(New_Job);
    }
    condition.notify_one();
}

5) Associa una funzione arbitraria alla tua coda

Pool_Obj.Add_Job(std::bind(&Some_Class::Some_Method, &Some_object));

Una volta integrati questi ingredienti, hai il tuo pool di threading dinamico. Questi thread vengono sempre eseguiti, in attesa del completamento del lavoro.

Mi scuso se ci sono alcuni errori di sintassi, ho digitato questi codici e ho una brutta memoria. Spiacenti, non posso fornirti il ​​codice completo del pool di thread, che violerebbe l'integrità del mio lavoro.

Modifica: per terminare il pool, chiamare il metodo shutdown ():

XXXX::shutdown(){
{
    unique_lock<mutex> lock(threadpool_mutex);
    terminate_pool = true;} // use this flag in condition.wait

    condition.notify_all(); // wake up all threads.

    // Join all threads.
    for(std::thread &every_thread : thread_vector)
    {   every_thread.join();}

    thread_vector.clear();  
    stopped = true; // use this flag in destructor, if not set, call shutdown() 
}

Come si ottiene un vettore <thread> quando thread (const thread &) = delete?
Christopher Pisz,

1
@ChristopherPisz std::vectornon richiede che i suoi elementi siano copiabili. È possibile utilizzare i vettori con i tipi di movimento solo ( unique_ptr, thread, future, etc.).
Daniel Langr,

nell'esempio sopra, come si interrompe la piscina? Dovresti condition.waitanche cercare una variabile stop_e controllare if (stop_ == true) { break;}?
John

@ John, per favore vedi il metodo di spegnimento sopra.
PhD AP EcE,

2
In shutdown (), dovrebbe essere thread_vector.clear (); invece di thread_vector.empty (); Corretta?
sudheerbb,

63

Un pool di thread significa che tutti i thread sono in esecuzione, sempre - in altre parole, la funzione thread non ritorna mai. Per dare ai thread qualcosa di significativo da fare, devi progettare un sistema di comunicazione tra thread, sia allo scopo di dire al thread che c'è qualcosa da fare, sia per comunicare i dati di lavoro reali.

In genere ciò implica un qualche tipo di struttura di dati simultanea e ogni thread presumibilmente dormirà su un qualche tipo di variabile di condizione, che verrebbe notificata quando c'è lavoro da fare. Alla ricezione della notifica, uno o più thread si riattivano, ripristinano un'attività dalla struttura di dati simultanea, la elaborano e memorizzano il risultato in modo analogo.

Il thread continuerebbe quindi per verificare se c'è ancora molto lavoro da fare e se non tornare a dormire.

Il risultato è che devi progettare tutto questo da solo, poiché non esiste una nozione naturale di "lavoro" universalmente applicabile. È un bel po 'di lavoro e ci sono alcuni problemi sottili che devi risolvere. (Puoi programmare in Go se ti piace un sistema che si occupa della gestione dei thread per te dietro le quinte.)


11
"devi progettare tutto questo da solo" <- questo è ciò che sto cercando di evitare di fare. Le goroutine sembrano fantastiche, però.
Yktula

2
@Yktula: Beh, è ​​un compito non banale. Dal tuo post non è nemmeno chiaro quale tipo di lavoro desideri svolgere, e questo è profondamente fondamentale per la soluzione. Puoi implementare Go in C ++, ma sarà una cosa molto specifica e metà delle persone si lamenterebbe che vorrebbe qualcosa di diverso.
Kerrek SB,

19

Un threadpool è al centro un insieme di thread tutti associati a una funzione che funziona come un ciclo di eventi. Questi thread attenderanno all'infinito l'esecuzione di un'attività o la loro stessa conclusione.

Il lavoro di threadpool è fornire un'interfaccia per inoltrare lavori, definire (e forse modificare) la politica di esecuzione di questi lavori (regole di pianificazione, istanza di thread, dimensione del pool) e monitorare lo stato dei thread e delle risorse correlate.

Quindi, per un pool versatile, bisogna iniziare definendo cosa è un'attività, come viene avviata, interrotta, qual è il risultato (vedere la nozione di promessa e futuro per quella domanda), che tipo di eventi i thread dovranno rispondere a, come li gestiranno, come questi eventi devono essere discriminati da quelli gestiti dai compiti. Questo può diventare piuttosto complicato come puoi vedere e imporre restrizioni su come funzioneranno i thread, poiché la soluzione diventa sempre più coinvolta.

L'attuale attrezzatura per la gestione degli eventi è piuttosto barebone (*): primitivi come mutex, variabili di condizione e alcune astrazioni (blocchi, barriere). Ma in alcuni casi, queste astrazioni potrebbero rivelarsi inadatte (vedi questa domanda correlata ) e si deve tornare a usare le primitive.

Anche altri problemi devono essere gestiti:

  • segnale
  • i / o
  • hardware (affinità del processore, configurazione eterogenea)

Come sarebbero questi nella tua impostazione?

Questa risposta a una domanda simile indica un'implementazione esistente pensata per boost e stl.

Ho offerto un'implementazione molto grezza di un threadpool per un'altra domanda, che non affronta molti problemi descritti sopra. Potresti voler costruire su di esso. Potresti anche voler dare un'occhiata ai framework esistenti in altre lingue, per trovare ispirazione.


(*) Non lo vedo come un problema, al contrario. Penso che sia lo spirito stesso del C ++ ereditato da C.


4
Follwoing [PhD EcE](https://stackoverflow.com/users/3818417/phd-ece) suggestion, I implemented the thread pool:

function_pool.h

#pragma once
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <cassert>

class Function_pool
{

private:
    std::queue<std::function<void()>> m_function_queue;
    std::mutex m_lock;
    std::condition_variable m_data_condition;
    std::atomic<bool> m_accept_functions;

public:

    Function_pool();
    ~Function_pool();
    void push(std::function<void()> func);
    void done();
    void infinite_loop_func();
};

function_pool.cpp

#include "function_pool.h"

Function_pool::Function_pool() : m_function_queue(), m_lock(), m_data_condition(), m_accept_functions(true)
{
}

Function_pool::~Function_pool()
{
}

void Function_pool::push(std::function<void()> func)
{
    std::unique_lock<std::mutex> lock(m_lock);
    m_function_queue.push(func);
    // when we send the notification immediately, the consumer will try to get the lock , so unlock asap
    lock.unlock();
    m_data_condition.notify_one();
}

void Function_pool::done()
{
    std::unique_lock<std::mutex> lock(m_lock);
    m_accept_functions = false;
    lock.unlock();
    // when we send the notification immediately, the consumer will try to get the lock , so unlock asap
    m_data_condition.notify_all();
    //notify all waiting threads.
}

void Function_pool::infinite_loop_func()
{
    std::function<void()> func;
    while (true)
    {
        {
            std::unique_lock<std::mutex> lock(m_lock);
            m_data_condition.wait(lock, [this]() {return !m_function_queue.empty() || !m_accept_functions; });
            if (!m_accept_functions && m_function_queue.empty())
            {
                //lock will be release automatically.
                //finish the thread loop and let it join in the main thread.
                return;
            }
            func = m_function_queue.front();
            m_function_queue.pop();
            //release the lock
        }
        func();
    }
}

main.cpp

#include "function_pool.h"
#include <string>
#include <iostream>
#include <mutex>
#include <functional>
#include <thread>
#include <vector>

Function_pool func_pool;

class quit_worker_exception : public std::exception {};

void example_function()
{
    std::cout << "bla" << std::endl;
}

int main()
{
    std::cout << "stating operation" << std::endl;
    int num_threads = std::thread::hardware_concurrency();
    std::cout << "number of threads = " << num_threads << std::endl;
    std::vector<std::thread> thread_pool;
    for (int i = 0; i < num_threads; i++)
    {
        thread_pool.push_back(std::thread(&Function_pool::infinite_loop_func, &func_pool));
    }

    //here we should send our functions
    for (int i = 0; i < 50; i++)
    {
        func_pool.push(example_function);
    }
    func_pool.done();
    for (unsigned int i = 0; i < thread_pool.size(); i++)
    {
        thread_pool.at(i).join();
    }
}

2
Grazie! Questo mi ha davvero aiutato a iniziare con operazioni di threading parallelo. Ho finito per usare una versione leggermente modificata della tua implementazione.
Robbie Capps,

3

Qualcosa del genere potrebbe aiutare (tratto da un'app funzionante).

#include <memory>
#include <boost/asio.hpp>
#include <boost/thread.hpp>

struct thread_pool {
  typedef std::unique_ptr<boost::asio::io_service::work> asio_worker;

  thread_pool(int threads) :service(), service_worker(new asio_worker::element_type(service)) {
    for (int i = 0; i < threads; ++i) {
      auto worker = [this] { return service.run(); };
      grp.add_thread(new boost::thread(worker));
    }
  }

  template<class F>
  void enqueue(F f) {
    service.post(f);
  }

  ~thread_pool() {
    service_worker.reset();
    grp.join_all();
    service.stop();
  }

private:
  boost::asio::io_service service;
  asio_worker service_worker;
  boost::thread_group grp;
};

Puoi usarlo in questo modo:

thread_pool pool(2);

pool.enqueue([] {
  std::cout << "Hello from Task 1\n";
});

pool.enqueue([] {
  std::cout << "Hello from Task 2\n";
});

Tieni presente che reinventare un efficiente meccanismo di accodamento asincrono non è banale.

Boost :: asio :: io_service è un'implementazione molto efficiente, o in realtà è una raccolta di wrapper specifici della piattaforma (ad esempio, avvolge le porte di completamento I / O su Windows).


2
È necessario un tale incremento con C ++ 11? Direi che non sarebbe std::threadsufficiente?
einpoklum,

Non esiste un equivalente stdper boost::thread_group. boost::thread_groupè una raccolta di boost::threadistanze. Ma ovviamente, è molto facile da sostituire boost::thread_groupcon uno vectordi std::threads.
Rustyx,

3

Modifica: questo ora richiede C ++ 17 e concetti. (A partire dal 9/12/16, è sufficiente solo g ++ 6.0+.)

La deduzione del modello è molto più accurata a causa di ciò, tuttavia, quindi vale la pena di ottenere un compilatore più recente. Non ho ancora trovato una funzione che richiede argomenti espliciti sul modello.

Ora prende anche qualsiasi oggetto richiamabile appropriato ( ed è ancora staticamente typesafe !!! ).

Ora include anche un pool di thread con priorità di threading verde opzionale che utilizza la stessa API. Questa classe è solo POSIX, tuttavia. Utilizza l' ucontext_tAPI per il cambio di attività di userspace.


Ho creato una semplice libreria per questo. Di seguito è riportato un esempio di utilizzo. (Sto rispondendo a questo perché era una delle cose che ho trovato prima di decidere che fosse necessario scriverlo da solo.)

bool is_prime(int n){
  // Determine if n is prime.
}

int main(){
  thread_pool pool(8); // 8 threads

  list<future<bool>> results;
  for(int n = 2;n < 10000;n++){
    // Submit a job to the pool.
    results.emplace_back(pool.async(is_prime, n));
  }

  int n = 2;
  for(auto i = results.begin();i != results.end();i++, n++){
    // i is an iterator pointing to a future representing the result of is_prime(n)
    cout << n << " ";
    bool prime = i->get(); // Wait for the task is_prime(n) to finish and get the result.
    if(prime)
      cout << "is prime";
    else
      cout << "is not prime";
    cout << endl;
  }  
}

Puoi passare asyncqualsiasi funzione con qualsiasi valore di ritorno (o vuoto) e qualsiasi argomento (o no) e restituirà un valore corrispondente std::future. Per ottenere il risultato (o semplicemente attendere il completamento di un'attività) si chiama get()il futuro.

Ecco il github: https://github.com/Tyler-Hardin/thread_pool .


1
Sembra incredibile, ma sarebbe bello avere un confronto con l'intestazione di vit-vit!
Jonathan H,

1
@ Sh3ljohn, da un'occhiata, sembra che siano sostanzialmente le stesse in API. vit-vit utilizza la coda lockfree di boost, che è migliore della mia. (Ma il mio obiettivo era specificamente quello di farlo solo con std :: *. Suppongo che potrei implementare la coda lockfree me stesso, ma suona duro e soggetto a errori.) Inoltre, vit-vit non ha un .cpp associato, che è più semplice da usare per le persone che non sanno cosa stanno facendo. (Ad esempio github.com/Tyler-Hardin/thread_pool/issues/1 )
Tyler

Lui / lei ha anche una sola soluzione stl che ho cercato nelle ultime ore, all'inizio sembrava più complicata della tua con puntatori condivisi dappertutto, ma questo è effettivamente necessario per gestire correttamente il ridimensionamento a caldo.
Jonathan H,

@ Sh3ljohn, ah, non ho notato il ridimensionamento a caldo. Bello. Ho scelto di non preoccuparmene perché non rientra realmente nel caso d'uso previsto. (Non riesco a pensare a un caso in cui vorrei ridimensionare, personalmente, ma ciò potrebbe essere dovuto a una mancanza di immaginazione.)
Tyler,

1
Esempio di caso d'uso: stai eseguendo un'API RESTful su un server e devi ridurre temporaneamente l'allocazione delle risorse a fini di manutenzione, senza dover arrestare completamente il servizio.
Jonathan H,

3

Questa è un'altra implementazione del pool di thread che è molto semplice, facile da capire e usare, usa solo la libreria standard C ++ 11 e può essere guardata o modificata per i tuoi usi, dovrebbe essere un buon inizio se vuoi entrare nell'uso del thread piscine:

https://github.com/progschj/ThreadPool


3

Puoi usare thread_pool dalla libreria boost:

void my_task(){...}

int main(){
    int threadNumbers = thread::hardware_concurrency();
    boost::asio::thread_pool pool(threadNumbers);

    // Submit a function to the pool.
    boost::asio::post(pool, my_task);

    // Submit a lambda object to the pool.
    boost::asio::post(pool, []() {
      ...
    });
}

Puoi anche usare threadpool dalla community open source:

void first_task() {...}    
void second_task() {...}

int main(){
    int threadNumbers = thread::hardware_concurrency();
    pool tp(threadNumbers);

    // Add some tasks to the pool.
    tp.schedule(&first_task);
    tp.schedule(&second_task);
}

1

Un threadpool senza dipendenze al di fuori di STL è del tutto possibile. Di recente ho scritto una piccola libreria threadpool solo intestazione per risolvere esattamente lo stesso problema. Supporta il ridimensionamento dinamico del pool (modifica del numero di lavoratori in fase di esecuzione), in attesa, in arresto, in pausa, ripresa e così via. Spero che lo trovi utile.


sembra che tu abbia eliminato il tuo account github (o che il link sia sbagliato). Hai spostato questo codice altrove?
rtpax,

1
@rtpax Ho spostato il repository - ho aggiornato la risposta per riflettere ciò.
cantordust,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.