C ++ 11'de iş parçacığı havuzu


131

İlgili sorular :

C ++ 11 hakkında:

Boost Hakkında:


Nasıl alırım parçacığı havuzu için görevleri göndermek oluşturma ve bunları tekrar tekrar silmeden? Bu, kalıcı iş parçacıklarının katılmadan yeniden eşitlenmesi anlamına gelir.


Şuna benzeyen bir kodum var:

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;
}

Her yinelemeyi oluşturmak ve birleştirmek yerine, görevleri her yinelemede çalışan iş parçacıklarına göndermeyi ve yalnızca bir kez oluşturmayı tercih ederim.


1
işte bir ilgili soru ve cevabım.
didierc

1
tbb kullanmayı düşündünüz (Intel'dir, ancak ücretsiz ve açık kaynaktır ve tam olarak istediğiniz şeyi yapar: yalnızca (yinelemeli olarak bölünebilir) görevler gönderir ve iş parçacıkları hakkında endişelenmeyin)?
Walter

2
Bu FOSS projesi benim bir iş parçacığı havuzu kitaplığı oluşturma girişimim, isterseniz kontrol edin. -> code.google.com/p/threadpool11
Etherealone

Tbb kullanmanın nesi yanlış?
Walter

Yanıtlar:


84

C ++ İş Parçacığı Havuzu Kitaplığını kullanabilirsiniz, https://github.com/vit-vit/ctpl .

Daha sonra yazdığınız kod aşağıdaki ile değiştirilebilir

#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);
    }
}

İstenilen sayıda iş parçacığı elde edeceksiniz ve bunları yinelemelerde tekrar tekrar oluşturmayacak ve silmeyeceksiniz.


11
Cevap bu olmalı; tek başlıklı, okunabilir, anlaşılır, özlü ve standartlarla uyumlu C ++ 11 kitaplığı. Harika iş!
Jonathan H

@ vit-vit işlevi olan bir örnek verebilir misiniz lütfen? nasıl bir itiyorsun sınıf üyesi işlevini deresults[j] = p.push([&arr, j](int){ arr[j] +=2; });
Hani Göç

1
@HaniGoc Sadece örneği referans alarak yakalayın.
Jonathan H

@ vit-vit STL versiyonunu iyileştirmeniz için size bir çekme isteği gönderdi.
Jonathan H

@ vit-vit: Bu kütüphanenin bakımcısıyla sorularla iletişime geçmek zor, ipucu.
einpoklum

83

Bu, cevabımdan çok benzer başka bir gönderiye kopyalandı, umarım yardımcı olabilir:

1) Bir sistemin destekleyebileceği maksimum iş parçacığı sayısıyla başlayın:

int Num_Threads =  thread::hardware_concurrency();

2) Etkili bir evre havuzu uygulaması için, Num_Threads'e göre evreler oluşturulduktan sonra, yenilerini yaratmamak veya eskilerini yok etmemek (katılarak) daha iyidir. Performans cezası olacaktır, hatta uygulamanızın seri sürüme göre daha yavaş gitmesine neden olabilir.

Her C ++ 11 iş parçacığı, işlevlerinde sonsuz bir döngü ile çalışıyor olmalı ve sürekli olarak yeni görevlerin yakalanmasını ve çalıştırılmasını beklemelidir.

İşte böyle bir işlevi iş parçacığı havuzuna nasıl ekleyeceğiniz:

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) Infinite_loop_function

Bu, görev kuyruğunu bekleyen bir "while (true)" döngüsüdür

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) Sıranıza iş eklemek için bir işlev yapın

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

5) Sıranıza rastgele bir işlevi bağlayın

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

Bu malzemeleri birleştirdikten sonra, kendi dinamik diş açma havuzunuz olur. Bu iş parçacıkları her zaman çalışır, yapılacak iş için beklerler.

Bazı sözdizimi hataları varsa özür dilerim, bu kodu yazdım ve hafızam bozuk. Maalesef size iş bütünlüğümü ihlal edecek eksiksiz iş parçacığı havuzu kodunu sağlayamıyorum.

Düzenleme: havuzu sonlandırmak için shutdown () yöntemini çağırın:

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() 
}

Thread (const thread &) = delete olduğunda <thread> vektörüne nasıl sahip olursunuz?
Christopher Pisz

1
@ChristopherPisz std::vector, öğelerinin kopyalanabilir olmasını gerektirmez. Sen hamle okunur türleri ile (vektörler kullanabilirsiniz unique_ptr, thread, futurevb.)
Daniel Langr

Yukarıdaki örneğinizde havuzu nasıl durdurursunuz? condition.waitAyrıca bir değişken aramalı stop_ve kontrol etmelidir if (stop_ == true) { break;}?
John

@John, lütfen yukarıdaki kapatma yöntemine bakın.
PhD AP EcE

2
Shutdown () içinde, thread_vector.clear () olmalıdır; thread_vector.empty () yerine; Doğru?
sudheerbb

63

Bir iş parçacığı havuzu, tüm iş parçacıklarınızın her zaman çalıştığı anlamına gelir - başka bir deyişle, iş parçacığı işlevi asla geri dönmez. İş parçacıkları için anlamlı bir şey vermek için, hem iş parçacığına yapılacak bir şey olduğunu söylemek hem de gerçek çalışma verilerini iletmek amacıyla bir iş parçacığı arası iletişim sistemi tasarlamalısınız.

Tipik olarak bu, bir tür eşzamanlı veri yapısını içerecektir ve her iş parçacığı muhtemelen yapılacak iş olduğunda bildirilecek olan bir tür koşul değişkeni üzerinde uyuyacaktır. Bildirimi aldıktan sonra, iş parçacıklarından biri veya birkaçı uyanır, eşzamanlı veri yapısından bir görevi kurtarır, işler ve sonucu benzer bir şekilde depolar.

İş parçacığı daha sonra yapılacak daha çok iş olup olmadığını ve eğer değilse uykuya dönüp gitmediğini kontrol etmeye devam eder.

Sonuç olarak, evrensel olarak geçerli olan doğal bir "iş" kavramı olmadığı için, tüm bunları kendiniz tasarlamanız gerekiyor. Oldukça uzun bir çalışma ve düzeltmeniz gereken bazı ince sorunlar var. (Perde arkasında sizin için iş parçacığı yönetimiyle ilgilenen bir sistemden hoşlanıyorsanız Go'da programlayabilirsiniz.)


11
"Bütün bunları kendiniz tasarlamalısınız" <- bunu yapmaktan kaçınmaya çalışıyorum. Yine de gorutinler harika görünüyor.
Yktula

2
@Yktula: Bu hiç de önemsiz olmayan bir görev. Gönderinizden ne tür bir işin yapılmasını istediğinizi bile net değil ve bu çözüm için son derece temeldir. Go'yu C ++ 'da uygulayabilirsiniz, ancak bu çok özel bir şey olacak ve insanların yarısı farklı bir şey isteyeceklerinden şikayet edecek.
Kerrek SB

19

Bir iş parçacığı havuzu, özünde tümü bir olay döngüsü olarak çalışan bir işleve bağlı bir dizi iş parçacığıdır. Bu iş parçacıkları, bir görevin yürütülmesini veya kendi sonlandırılmasını sonsuza kadar bekleyecektir.

İş parçacığı havuzu işi, işleri göndermek, bu işleri çalıştırma ilkesini (zamanlama kuralları, iş parçacığı başlatımı, havuzun boyutu) tanımlamak (ve belki de değiştirmek) ve iş parçacığı ve ilgili kaynakların durumunu izlemek için bir arabirim sağlamaktır.

Dolayısıyla, çok yönlü bir havuz için, bir görevin ne olduğunu, nasıl başlatıldığını, kesintiye uğradığını, sonucun ne olduğunu (bu soru için vaat ve gelecek kavramına bakın), konu başlıklarının ne tür olaylara yanıt vermesi gerektiğini tanımlayarak başlamalıdır. bunlarla nasıl başa çıkacaklarına, bu olayların görevler tarafından ele alınanlardan nasıl ayırt edileceğine. Gördüğünüz gibi bu oldukça karmaşık hale gelebilir ve çözüm gitgide daha karmaşık hale geldikçe iş parçacıklarının nasıl çalışacağına dair kısıtlamalar getirebilir.

Olayları işlemek için mevcut araçlar oldukça barebone'lardır (*): muteksler, koşul değişkenleri gibi ilkel öğeler ve bunun üzerine birkaç soyutlama (kilitler, engeller). Ancak bazı durumlarda, bu soyutlamalar uygun olmayabilir (bu ilgili soruya bakınız ) ve kişi ilkelleri kullanmaya geri dönmelidir .

Diğer sorunların da yönetilmesi gerekir:

  • işaret
  • I / O
  • donanım (işlemci yakınlığı, heterojen kurulum)

Sizin ortamınızda bunlar nasıl oynanır?

Benzer bir soruya verilen bu cevap , boost ve stl anlamına gelen mevcut bir uygulamaya işaret ediyor.

Başka bir soru için bir iş parçacığı havuzunun çok kaba bir uygulamasını önerdim , bu yukarıda belirtilen birçok sorunu ele almıyor. Üzerine inşa etmek isteyebilirsin. İlham almak için diğer dillerdeki mevcut çerçevelere de göz atmak isteyebilirsiniz.


(*) Bunu bir sorun olarak görmüyorum, tam tersine. Bence C ++ 'nın ruhu C'den miras kaldı.


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
Teşekkürler! Bu, paralel diş açma işlemlerine başlamama gerçekten yardımcı oldu. Sonunda uygulamanızın biraz değiştirilmiş bir sürümünü kullandım.
Robbie Capps

3

Bunun gibi bir şey yardımcı olabilir (çalışan bir uygulamadan alınmıştır).

#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;
};

Bunu şu şekilde kullanabilirsiniz:

thread_pool pool(2);

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

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

Etkili bir eşzamansız kuyruk mekanizmasını yeniden icat etmenin önemsiz olmadığını unutmayın.

Boost :: asio :: io_service çok verimli bir uygulama veya aslında platforma özgü sarmalayıcılardan oluşan bir koleksiyondur (örneğin, Windows'ta G / Ç tamamlama bağlantı noktalarını sarar).


2
C ++ 11 ile bu kadar destek gerekli mi? Yeterli değil mi std::thread?
einpoklum

Hiçbir eşdeğer yoktur stdiçin boost::thread_group. örneklerden boost::thread_groupoluşan bir koleksiyondur boost::thread. Ama elbette, boost::thread_groupa vector/ std::threads ile değiştirmek çok kolaydır .
rustyx

3

Düzenleme: Bu artık C ++ 17 ve kavramları gerektirir. (12.09.2016 itibariyle yalnızca g ++ 6.0+ yeterlidir.)

Şablon çıkarımı bundan dolayı çok daha doğrudur, bu yüzden daha yeni bir derleyici edinme çabasına değer. Henüz açık şablon bağımsız değişkenleri gerektiren bir işlev bulamadım.

Ayrıca artık uygun herhangi bir çağrılabilir nesneyi alır ( ve hala statik olarak güvenli !!! ).

Ayrıca artık aynı API'yi kullanan isteğe bağlı bir yeşil iş parçacığı öncelikli iş parçacığı havuzu da içeriyor. Bu sınıf yalnızca POSIX'tir. Kullanıcı ucontext_talanı görev değiştirme için API kullanır .


Bunun için basit bir kitaplık oluşturdum. Aşağıda bir kullanım örneği verilmiştir. (Bunu yanıtlıyorum çünkü kendim yazmam gerektiğine karar vermeden önce bulduğum şeylerden biriydi.)

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;
  }  
}

asyncHerhangi bir (veya geçersiz) dönüş değeri ve herhangi bir (veya hiçbir) argüman içeren herhangi bir işlevi iletebilirsiniz ve karşılık gelen bir döndürür std::future. Sonucu almak için (veya sadece bir görev tamamlanana kadar bekleyin) arayınget() geleceği .

İşte github: https://github.com/Tyler-Hardin/thread_pool .


1
Harika görünüyor, ancak vit-vit'nin başlığıyla bir karşılaştırma yapmak harika olurdu!
Jonathan H

1
@ Sh3ljohn, ona bakıldığında, temelde API'de aynı oldukları anlaşılıyor. vit-vit, benimkinden daha iyi olan boost'un kilitsiz kuyruğunu kullanır. (Ama amacım bunu özellikle std :: * ile yapmaktı. Sanırım kilitsiz kuyruğu kendim uygulayabilirim, ancak bu kulağa zor geliyor ve hataya açık.) Ayrıca, vit-vit'nin ilişkili bir .cpp'si yok. ne yaptığını bilmeyen insanlar için kullanımı daha kolaydır. (Örneğin github.com/Tyler-Hardin/thread_pool/issues/1 )
Tyler

Ayrıca son birkaç saattir çatal kullandığım sadece tek bir çözüme sahip, ilk başta her yerde paylaşılan işaretçilerle sizinkinden daha karmaşık görünüyordu, ancak bu aslında sıcak yeniden boyutlandırmayı düzgün bir şekilde ele almak için gerekli.
Jonathan H

@ Sh3ljohn, ah, sıcak yeniden boyutlandırmayı fark etmedim. Bu iyi. Bunun için endişelenmemeyi seçtim çünkü gerçekten amaçlanan kullanım durumu dahilinde değil. (Kişisel olarak yeniden boyutlandırmak isteyeceğim bir durum düşünemiyorum, ancak bu hayal gücünün eksikliğinden kaynaklanıyor olabilir.)
Tyler

1
Örnek kullanım durumu: Bir sunucuda bir RESTful API çalıştırıyorsunuz ve hizmetin tamamen kapatılmasına gerek kalmadan bakım amacıyla kaynak tahsisini geçici olarak azaltmanız gerekiyor.
Jonathan H

3

Bu, çok basit, anlaşılması ve kullanımı kolay, yalnızca C ++ 11 standart kitaplığı kullanan ve kullanımlarınız için bakılabilen veya değiştirilebilen başka bir iş parçacığı havuzu uygulamasıdır, iş parçacığını kullanmaya başlamak istiyorsanız güzel bir başlangıç ​​olmalıdır. havuzlar:

https://github.com/progschj/ThreadPool


3

Boost kitaplığından thread_pool'u kullanabilirsiniz :

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, []() {
      ...
    });
}

Açık kaynak topluluğundaki iş parçacığı havuzunu da kullanabilirsiniz :

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

STL dışında bağımlılıkları olmayan bir iş parçacığı havuzu tamamen mümkündür. Yakın zamanda aynı sorunu çözmek için küçük bir salt başlık içeren iş parçacığı kitaplığı yazdım . Dinamik havuz yeniden boyutlandırmayı (çalışma zamanında çalışan sayısını değiştirme), beklemeyi, durdurmayı, duraklatmayı, devam ettirmeyi vb. Destekler. Umarım yararlı bulursunuz.


Görünüşe göre github hesabınızı silmişsiniz (veya bağlantıyı yanlış almışsınız). Bu kodu başka bir yere taşıdınız mı?
rtpax

1
@rtpax Depoyu taşıdım - cevabı bunu yansıtacak şekilde güncelledim.
cantordust
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.