Об'єднання ниток в C ++ 11


131

Відповідні питання :

Про C ++ 11:

Про Boost:


Як мені отримати пул потоків, на який можна надсилати завдання , не створюючи і не видаляючи їх знову і знову? Це означає стійкі потоки для повторної синхронізації без з'єднання.


У мене є такий код:

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

Замість того, щоб створювати та приєднувати нитки до кожної ітерації, я вважаю за краще надсилати завдання моїм робочим потокам кожну ітерацію та створювати їх лише один раз.


1
ось пов'язане питання і моя відповідь.
didierc

1
задумався про використання tbb (це Intel, але безкоштовно та з відкритим кодом, і робить саме те, що ви хочете: ви просто подаєте (рекурсивно поділяються) завдання і не хвилюєтесь про потоки)?
Вальтер

2
Цей проект FOSS - це моя спроба створити бібліотеку пулових потоків, перевірте це, якщо ви хочете. -> code.google.com/p/threadpool11
Etherealone

Що не так з використанням tbb?
Вальтер

Відповіді:


84

Ви можете скористатися бібліотекою басейнів ниток C ++, https://github.com/vit-vit/ctpl .

Тоді код, який ви написали, можна замінити наступним

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

Ви отримаєте потрібну кількість потоків і не будете створювати та видаляти їх повторно і знову на ітераціях.


11
Це має бути відповіддю; одноголовна, читана, проста, лаконічна та стандартна C ++ 11 бібліотека. Чудова робота!
Джонатан Н

@ vit-vit чи можете ви навести приклад із функцією? як ти results[j] = p.push([&arr, j](int){ arr[j] +=2; });
штовхаєш

1
@HaniGoc Просто захопіть екземпляр за посиланням.
Джонатан Н

@ vit-vit Надіслав вам запит на покращення версії STL.
Джонатан Н

@ vit-vit: Важко звернутися до керівника бібліотеки з питаннями, підказка.
einpoklum

83

Це скопійовано з моєї відповіді на інший дуже схожий пост, сподіваюся, що це може допомогти:

1) Почніть з максимальної кількості потоків, яку система може підтримувати:

int Num_Threads =  thread::hardware_concurrency();

2) Для ефективної реалізації потокової нитки, коли нитки створені відповідно до Num_Threads, краще не створювати нових або руйнувати старі (приєднавшись). Буде застосовано покарання за ефективність, можливо, навіть ваша програма подаватиме повільніше, ніж серіальна версія.

Кожен потік C ++ 11 повинен працювати у своїй функції з нескінченним циклом, постійно чекаючи нових завдань, щоб схопити та запустити.

Ось як приєднати таку функцію до пулу потоків:

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_

Це цикл "while (true)", який чекає черги завдань

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) Створіть функцію, щоб додати завдання до своєї черги

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

5) Прив’яжіть до своєї черги довільну функцію

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

Після інтеграції цих інгредієнтів у вас є власний динамічний пул для нарізки різьби. Ці нитки завжди працюють, чекаючи роботи.

Прошу вибачення, якщо є якісь синтаксичні помилки, я набрав цей код і у мене погана пам'ять. Вибачте, що я не можу надати вам повний код пулу потоків, який би порушив цілісність моєї роботи.

Редагувати: щоб припинити пул, виклик методу 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() 
}

Як у вас вектор <thread>, коли нитка (const нитка &) = видалити?
Крістофер Піс

1
@ChristopherPisz std::vectorне вимагає копіювання його елементів. Ви можете використовувати вектори з ходу тільки типів ( unique_ptr, thread, futureі т.д.).
Даніель Ленгр

у вашому вище прикладі, як зупинити басейн? Чи слід condition.waitтакож шукати змінну stop_і перевіряти if (stop_ == true) { break;}?
Джон

@John, будь ласка, дивіться спосіб вимкнення вище.
Кандидат наук AP EcE

2
У режимі shutdown (), він повинен бути thread_vector.clear (); замість thread_vector.empty (); Правильно?
sudheerbb

63

Пул потоків означає, що всі ваші потоки запущені, весь час - іншими словами, функція потоку ніколи не повертається. Щоб надати потокам щось важливе для цього, ви повинні розробити систему міжпотокового зв'язку, як з метою сказати потоці, що потрібно щось робити, так і для передачі фактичних даних про роботу.

Як правило, це буде включати якусь структуру одночасних даних, і кожен потік, ймовірно, спить на певній змінній умови, про яку буде повідомлено, коли потрібно виконати роботу. Отримавши сповіщення, один або кілька потоків прокидаються, відновлюють завдання з паралельної структури даних, обробляють його та зберігають результат аналогічно.

Потім нитка продовжує перевіряти, чи потрібно ще працювати, а якщо не повернутись до сну.

Підсумок полягає в тому, що ви повинні все це спроектувати самостійно, оскільки не існує природного поняття "робота", яке є загальноприйнятим. Це зовсім небагато роботи, і є кілька тонких питань, з якими ви маєте виправитись. (Ви можете запрограмувати програму Go, якщо вам подобається система, яка піклується про управління потоками за кадром.)


11
"ти повинен все це створити сам" <- саме цього я намагаюся уникати. Гороутини здаються фантастичними.
Yktula

2
@Yktula: Ну, це дуже нетривіальне завдання. З посади навіть не зрозуміло, яку роботу ви хочете виконати, і це дуже важливо для рішення. Ви можете реалізувати Go в C ++, але це буде дуже конкретна річ, і половина людей скаржиться, що хочуть чогось іншого.
Керрек СБ

19

Пул потоку - це сукупність ниток, всі пов'язані з функцією, що працює як цикл подій. Ці потоки будуть нескінченно чекати виконання завдання або їх власного припинення.

Завдання потокової низки полягає у наданні інтерфейсу для подання завдань, визначення (а можливо, зміни) політики виконання цих завдань (правила планування, інстанціалізація потоку, розмір пулу) та моніторинг стану потоків та пов'язаних з ними ресурсів.

Отже, для універсального пулу потрібно почати з визначення того, що таке завдання, як воно запускається, переривається, який результат (див. Поняття обіцянки та майбутнє для цього питання), на які події нитки повинні відповідати до того, як вони будуть поводитися з ними, як ці події повинні відрізнятися від тих, що вирішуються завданнями. Це може стати досить складним, як ви бачите, і накласти обмеження на те, як будуть працювати нитки, оскільки рішення все більше залучається.

Поточний інструмент для обробки подій є досить босоніжними (*): примітиви, як мутекси, змінні стану та кілька абстракцій поверх цього (замки, бар'єри). Але в деяких випадках ці абстракції можуть виявитися непридатними (див. Це пов'язане питання ), і треба повернутися до використання примітивів.

Інші проблеми також мають бути вирішені:

  • сигнал
  • я / о
  • апаратне забезпечення (спорідненість процесора, гетерогенна настройка)

Як вони могли б розігратися у ваших умовах?

Ця відповідь на аналогічне запитання вказує на існуючу реалізацію, призначену для boost та stl.

Я запропонував дуже грубу реалізацію потокової низки для іншого питання, яке не стосується багатьох проблем, описаних вище. Можливо, ви захочете наростити це. Ви також можете ознайомитися з існуючими рамками іншими мовами, щоб знайти натхнення.


(*) Я не бачу це як проблему, навпаки. Я думаю, що це сам дух C ++, успадкований від 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
Дякую! Це дійсно допомогло мені почати з паралельних операцій з нарізкою різьби. Я в кінцевому рахунку використовував трохи модифіковану версію вашої реалізації.
Роббі Капс

3

Щось подібне може допомогти (взяте з робочого додатка).

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

Ви можете використовувати його так:

thread_pool pool(2);

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

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

Майте на увазі, що винахід ефективного асинхронного механізму черги не є дрібницею.

Boost :: asio :: io_service - це дуже ефективна реалізація, або насправді це колекція обгортків, що залежать від платформи (наприклад, вона завершує порти завершення вводу-виводу в Windows).


2
Чи потрібен такий великий приріст із C ++ 11? Хіба, скажімо, це не std::threadвистачить?
einpoklum

Не існує еквівалента stdдля boost::thread_group. boost::thread_groupце сукупність boost::threadекземплярів. Але, звичайно, це дуже легко замінити boost::thread_groupз vectorз std::threadс.
іржі

3

Редагувати: для цього зараз потрібно C ++ 17 та поняття. (Станом на 12.12.16 р. Достатньо лише g ++ 6.0+.)

Вирахування шаблону набагато точніше через нього, тому варто докласти зусиль, щоб отримати новий компілятор. Я ще не знайшов функції, яка вимагає явних аргументів шаблону.

Тепер він також приймає будь-який відповідний об'єкт, що викликається ( і досі статично безпечний !!! ).

Тепер він також включає необов'язковий зелений пул пріоритетних ниток з ниткою з використанням того ж API. Хоча цей клас є лише POSIX. Він використовує ucontext_tAPI для перемикання завдань у просторі користувачів.


Я створив для цього просту бібліотеку. Приклад використання наведено нижче. (Я відповідаю на це, тому що це було одне з речей, які я знайшов, перш ніж вирішив, що це потрібно написати сам.)

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

Ви можете передавати asyncбудь-яку функцію з будь-яким (або недійсним) значенням повернення та будь-якими (або ні) аргументами, і воно поверне відповідне std::future. Щоб отримати результат (або просто почекати, поки завдання не буде виконано), ви закликаєте get()майбутнє.

Ось github: https://github.com/Tyler-Hardin/thread_pool .


1
Виглядає дивовижно, але було б чудово порівняти із заголовком vit-vit!
Джонатан Н

1
@ Sh3ljohn, поглянувши на це, схоже, що вони в API однакові. vit-vit використовує безстрокову чергу boost, яка краща за мою. (Але моя мета полягала саме в тому, щоб це зробити тільки з std :: *. Я думаю, я міг би реалізувати чергу lockfree самостійно, але це звучить важко і схильно до помилок.) Також vit-vit не має асоційованого .cpp, який простіше використовувати для людей, які не знають, що роблять. (Наприклад github.com/Tyler-Hardin/thread_pool/issues/1 )
Тайлер

У нього / вона також є рішення, призначене лише для stl, яке я розглядав протягом останніх кількох годин, спочатку воно виглядало складніше, ніж ваше, із загальними вказівниками всюди, але це насправді потрібно, щоб правильно обробити розмір гарячого розміру.
Джонатан Н

@ Sh3ljohn, ах, я не помітив гарячих розмірів. Це мило. Я вирішив не турбуватися про це, оскільки це насправді не в межах передбаченого випадку використання. (Я не можу придумати випадок, коли я хотів би змінити розмір особисто, але це могло бути через брак уяви.)
Тайлер

1
Приклад використання-випадку: ви працюєте на сервері API RESTful і вам потрібно тимчасово зменшити розподіл ресурсів для цілей технічного обслуговування, не потребуючи повного вимкнення служби.
Джонатан Н

3

Це ще одна реалізація пулу потоків, яка дуже проста, проста у розумінні та використанні, використовує лише стандартну бібліотеку C ++ 11 і може бути переглянута або модифікована для ваших цілей. басейни:

https://github.com/progschj/ThreadPool


3

Ви можете використовувати thread_pool з бібліотеки підвищення:

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

Ви також можете використовувати потоковий пул із спільноти з відкритим кодом:

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 цілком можливий. Нещодавно я написав невелику бібліотеку з нитками для заголовка, щоб вирішити ту саму проблему. Він підтримує динамічний розмір пулу (зміна кількості робітників під час виконання), очікування, зупинка, пауза, відновлення тощо. Сподіваюсь, вам це стане в нагоді.


виглядає так, що ви видалили свій обліковий запис github (або помилку отримали). Ви перенесли цей код кудись ще?
rtpax

1
@rtpax Я перемістив репо - я оновив відповідь, щоб це відобразити.
cantordust
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.