Search code examples
c++multithreadingboostthreadpoolboost-asio

C++ thread pool using boost::asio::thread_pool, why can't I reuse my threads?


I am experimenting with boost::asio::thread_pool to create a thread pool in my application. I created the following toy example to see if I understand how it works but clearly not :)

#include <boost/asio/post.hpp>
#include <boost/asio/thread_pool.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::asio::thread_pool g_pool(10);

void f(int i) {
    std::cout << i << "\n";
}

int main() {
    for (size_t i = 0; i != 50; ++i) {
        boost::asio::post(g_pool, boost::bind(f, 10 * i));
        g_pool.join();
    }
}

The program outputs

0

I am puzzled by two things: One, if I'm waiting for the threads to finish using g_pool.join(), why can I then not reuse the threads in the next iteration. I.e., I expected to also see the numbers 10,20,30,... printed in subsequent iterations etc.

Secondly, I'm creating a thread pool of size 10, why am I not at least seeing 10 outputs then? I cannot wrap my head around this.

Please let me know where I am going wrong, thanks in advance!


Solution

  • You join the pool after posting the first task. So, the pool stops before you even accept a second task. That explains why you're not seeing more.

    This fixes that:

    for (size_t i = 0; i != 50; ++i) {
        post(g_pool, boost::bind(f, 10 * i));
    }
    g_pool.join();
    

    Addendum #1

    In response to the comments. In case you want to wait for the outcome of a specific task, consider a future:

    Live On Coliru

    #include <boost/asio.hpp>
    #include <boost/bind/bind.hpp>
    #include <boost/thread.hpp>
    #include <iostream>
    #include <future>
    
    boost::asio::thread_pool g_pool(10);
    
    int f(int i) {
        std::cout << '(' + std::to_string(i) + ')';
        return i * i;
    }
    
    int main() {
        std::cout << std::unitbuf;
        std::future<int> answer;
    
        for (size_t i = 0; i != 50; ++i) {
            auto task = boost::bind(f, 10 * i);
            if (i == 42) {
                answer = post(g_pool, std::packaged_task<int()>(task));
            } else
            {
                post(g_pool, task);
            }
        }
    
        answer.wait(); // optionally make sure it is ready before blocking get()
        std::cout << "\n[Answer to #42: " + std::to_string(answer.get()) + "]\n";
    
        // wait for remaining tasks
        g_pool.join();
    }
    

    With one possible output:

    (0)(50)(30)(90)(110)(100)(120)(130)(140)(150)(160)(170)(180)(190)(40)(200)(210)(220)(240)(250)(70)(260)(20)(230)(10)(290)(80)(270)(300)(340)(350)(310)(360)(370)(380)(330)(400)(410)(430)(60)(420)(470)(440)(490)(480)(320)(460)(450)(390)
    [Answer to #42: 176400]
    (280)
    

    Addendum #2: Serializing tasks

    If you want to serialize specific tasks, you can use a strand. E.g. to serialize all the request based on the remainder of the parameter modulo 3:

    Live On Coliru

    #include <boost/asio.hpp>
    #include <boost/bind/bind.hpp>
    #include <boost/thread.hpp>
    #include <iostream>
    #include <future>
    
    boost::asio::thread_pool g_pool(10);
    
    int f(int i) {
        std::cout << '(' + std::to_string(i) + ')';
        return i * i;
    }
    
    int main() {
        std::cout << std::unitbuf;
        
        std::array strands{make_strand(g_pool.get_executor()),
                           make_strand(g_pool.get_executor()),
                           make_strand(g_pool.get_executor())};
    
        for (size_t i = 0; i != 50; ++i) {
            post(strands.at(i % 3), boost::bind(f, i));
        }
    
        g_pool.join();
    }
    

    With a possible output:

    (0)(3)(6)(2)(9)(1)(5)(8)(11)(4)(7)(10)(13)(16)(19)(22)(25)(28)(31)(34)(37)(40)(43)(46)(49)(12)(15)(14)(18)(21)(24)(27)(30)(33)(36)(39)(42)(45)(48)(17)(20)(23)(26)(29)(32)(35)(38)(41)(44)(47)
    

    Note that all work is done on any thread, but tasks on a strand happen in the order in which they were posted. So,

    • 0, 3, 6, 9, 12...
    • 1, 4, 7, 10, 13...
    • 2, 5, 8, 11, 14...

    happen strictly serially, though

    • 4 and 7 don't need to happen on the same physical thread
    • 11 might happen before 4, because they're not on the same strand

    Even More

    In case you need more "barrier-like" synchronization, or what's known as fork-join semantics, see Boost asio thread_pool join does not wait for tasks to be finished (where I posted two answers, one after I discovered the fork-join executor example).