29

I need to parallelize some tasks in a C++ program and am completely new to parallel programming. I've made some progress through internet searches so far, but am a bit stuck now. I'd like to reuse some threads in a loop, but clearly don't know how to do what I'm trying for.

I am acquiring data from two ADC cards on the computer (acquired in parallel), then I need to perform some operations on the collected data (processed in parallel) while collecting the next batch of data. Here is some pseudocode to illustrate

//Acquire some data, wait for all the data to be acquired before proceeding
std::thread acq1(AcquireData, boardHandle1, memoryAddress1a);
std::thread acq2(AcquireData, boardHandle2, memoryAddress2a);
acq1.join();
acq2.join();

while(user doesn't interrupt)
{

//Process first batch of data while acquiring new data
std::thread proc1(ProcessData,memoryAddress1a);
std::thread proc2(ProcessData,memoryAddress2a);
acq1(AcquireData, boardHandle1, memoryAddress1b);
acq2(AcquireData, boardHandle2, memoryAddress2b);
acq1.join();
acq2.join();
proc1.join();
proc2.join();
/*Proceed in this manner, alternating which memory address 
is written to and being processed until the user interrupts the program.*/
}

That's the main gist of it. The next run of the loop would write to the "a" memory addresses while processing the "b" data and continue to alternate (I can get the code to do that, just took it out to prevent cluttering up the problem).

Anyway, the problem (as I'm sure some people can already tell) is that the second time I try to use acq1 and acq2, the compiler (VS2012) says "IntelliSense: call of an object of a class type without appropriate operator() or conversion functions to pointer-to-function type". Likewise, if I put std::thread in front of acq1 and acq2 again, it says " error C2374: 'acq1' : redefinition; multiple initialization".

So the question is, can I reassign threads to a new task when they have completed their previous task? I always wait for the previous use of the thread to end before calling it again, but I don't know how to reassign the thread, and since it's in a loop, I can't make a new thread each time (or if I could, that seems wasteful and unnecessary, but I could be mistaken).

Thanks in advance

notaCSmajor
  • 291
  • 1
  • 3
  • 3

6 Answers6

43

The easiest way is to use a waitable queue of std::function objects. Like this:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <chrono>


class ThreadPool
{
    public:

    ThreadPool (int threads) : shutdown_ (false)
    {
        // Create the specified number of threads
        threads_.reserve (threads);
        for (int i = 0; i < threads; ++i)
            threads_.emplace_back (std::bind (&ThreadPool::threadEntry, this, i));
    }

    ~ThreadPool ()
    {
        {
            // Unblock any threads and tell them to stop
            std::unique_lock <std::mutex> l (lock_);

            shutdown_ = true;
            condVar_.notify_all();
        }

        // Wait for all threads to stop
        std::cerr << "Joining threads" << std::endl;
        for (auto& thread : threads_)
            thread.join();
    }

    void doJob (std::function <void (void)> func)
    {
        // Place a job on the queu and unblock a thread
        std::unique_lock <std::mutex> l (lock_);

        jobs_.emplace (std::move (func));
        condVar_.notify_one();
    }

    protected:

    void threadEntry (int i)
    {
        std::function <void (void)> job;

        while (1)
        {
            {
                std::unique_lock <std::mutex> l (lock_);

                while (! shutdown_ && jobs_.empty())
                    condVar_.wait (l);

                if (jobs_.empty ())
                {
                    // No jobs to do and we are shutting down
                    std::cerr << "Thread " << i << " terminates" << std::endl;
                    return;
                 }

                std::cerr << "Thread " << i << " does a job" << std::endl;
                job = std::move (jobs_.front ());
                jobs_.pop();
            }

            // Do the job without holding any locks
            job ();
        }

    }

    std::mutex lock_;
    std::condition_variable condVar_;
    bool shutdown_;
    std::queue <std::function <void (void)>> jobs_;
    std::vector <std::thread> threads_;
};

void silly (int n)
{
    // A silly job for demonstration purposes
    std::cerr << "Sleeping for " << n << " seconds" << std::endl;
    std::this_thread::sleep_for (std::chrono::seconds (n));
}

int main()
{
    // Create two threads
    ThreadPool p (2);

    // Assign them 4 jobs
    p.doJob (std::bind (silly, 1));
    p.doJob (std::bind (silly, 2));
    p.doJob (std::bind (silly, 3));
    p.doJob (std::bind (silly, 4));
}
David Schwartz
  • 166,415
  • 16
  • 184
  • 259
  • 2
    Nice KISS solution with no extra trimmings. – Surt Oct 20 '16 at 22:00
  • For infinite loop functions (silly) this thread pool is not concurrent. – Amir Fo Jun 20 '19 at 08:06
  • @AmirForsati I don't understand what you mean. – David Schwartz Jun 25 '19 at 22:14
  • Imagine we ham functions that are managing i/o of users socket. We will use infinite loops `while(true)` inside of these functions to read and write to users stream. Only the first function will be executed others will wait in the queue! the concept of thread pool is having threads that are executing tasks concurrent by switching between tasks. – Amir Fo Jun 26 '19 at 12:21
  • @AmirForsati Try it out yourself and you will see that this is incorrect. By setting the number of threads in the pool, you set the number of tasks that will run concurrently. The tasks will run concurrently (at the concurrency level you choose) and your system will switch between tasks if the number of threads exceeds the number of cores. – David Schwartz Jun 26 '19 at 14:08
  • This is more a question than anything else, but in your `doJob` method, does the lock get released before (or when) you call `condVar_.notify_one()`? The reason I ask is because my intuition tells me it's possible that the notified thread wakes, tries the lock and fails before the lock is actually released by the thread executing `doJob`. – David Oct 25 '19 at 17:22
  • @David The lock gets released after the call to `notify_one` because the lock holder (`l`) goes out of scope. I don't know what you mean by "and fails". All attempts to acquire the lock in this code block until the lock can be acquired, so they never fail. No code here ever holds the lock for very long, so no thread should ever encounter difficulty getting the lock, and if it did, it would only be because lost of other threads were making forward progress. – David Schwartz May 03 '20 at 19:52
  • @DavidSchwartz Thank you for responding to my comment. What I meant by "fail" is that it fails to acquire the lock (temporarily). I am making the assumption that when `notify_one` is called that it is possible for the scheduler to schedule one of the waiting threads (before the lock is released). This sounds like it would result in the thread attempting the lock and not getting it, leading to a slight performance penalty. – David May 15 '20 at 05:50
  • @DavidSchwartz This SO post is perhaps a good one to demonstrate what I was asking about (mutex contention). https://stackoverflow.com/a/17102100/4882724 – David May 15 '20 at 06:15
  • @David Quite the opposite, it would be a huge performance win. Rather than scheduling a thread that wants to contend for the lock, the scheduler would schedule some other thread that can run without any contention. There would only be a performance penalty if the system had nothing useful to do, in which case, we don't really care much about performance. – David Schwartz May 15 '20 at 06:15
  • @David Also, that answer you linked to is completely wrong for modern pthreads implementations. Any sane implementation will know whether a thread that calls `pthread_cond_signal` holds the mutex or not and, if it does, will know that no thread could possibly be made ready-to-run since it needs the mutex to make forward progress. So it's actually more efficient to signal while holding the mutex as that can't make any thread ready-to-run and will not make any thread contend for the mutex. The unlock of the mutex, of course, will. But that has to anyway. – David Schwartz May 15 '20 at 06:18
  • @DavidSchwartz Thank you for clarifying. I appreciate the quick response to my questions. – David May 15 '20 at 06:20
  • @David Glad to help. As a general rule, write your code as cleanly and straightforwardly as you can. Trust the implementation to get it right if you do that. It takes many years of experience to have any hope of getting better performance with synchronization complexity and most things you would think are optimizations make things worse for complex and subtle reasons. – David Schwartz May 15 '20 at 06:23
16

The std::thread class is designed to execute exactly one task (the one you give it in the constructor) and then end. If you want to do more work, you'll need a new thread. As of C++11, that's all we have. Thread pools didn't make it into the standard. (I'm uncertain what C++14 has to say about them.)

Fortunately, you can easily implement the required logic yourself. Here is the large-scale picture:

  • Start n worker threads that all do the following:
    • Repeat while there is more work to do:
      • Grab the next task t (possibly waiting until one becomes ready).
      • Process t.
  • Keep inserting new tasks in the processing queue.
  • Tell the worker threads that there is nothing more to do.
  • Wait for the worker threads to finish.

The most difficult part here (which is still fairly easy) is properly designing the work queue. Usually, a synchronized linked list (from the STL) will do for this. Synchronized means that any thread that wishes to manipulate the queue must only do so after it has acquired a std::mutex so to avoid race conditions. If a worker thread finds the list empty, it has to wait until there is some work again. You can use a std::condition_variable for this. Each time a new task is inserted into the queue, the inserting thread notifies a thread that waits on the condition variable and will therefore stop blocking and eventually start processing the new task.

The second not-so-trivial part is how to signal to the worker threads that there is no more work to do. Clearly, you can set some global flag but if a worker is blocked waiting at the queue, it won't realize any time soon. One solution could be to notify_all() threads and have them check the flag each time they are notified. Another option is to insert some distinct “toxic” item into the queue. If a worker encounters this item, it quits itself.

Representing a queue of tasks is straight-forward using your self-defined task objects or simply lambdas.

All of the above are C++11 features. If you are stuck with an earlier version, you'll need to resort to third-party libraries that provide multi-threading for your particular platform.

While none of this is rocket science, it is still easy to get wrong the first time. And unfortunately, concurrency-related bugs are among the most difficult to debug. Starting by spending a few hours reading through the relevant sections of a good book or working through a tutorial can quickly pay off.

5gon12eder
  • 21,864
  • 5
  • 40
  • 85
  • Thanks for this answer, very thorough and well written. I'm going to save this as plan B if luk32's response below doesn't work out for me. Yours is probably the 'right' way to go about doing things. – notaCSmajor Oct 23 '14 at 13:38
0

This

 std::thread acq1(...)

is the call of an constructor. constructing a new object called acq1

This

  acq1(...)

is the application of the () operator on the existing object aqc1. If there isn't such a operator defined for std::thread the compiler complains.

As far as I know you may not reused std::threads. You construct and start them. Join with them and throw them away,

Oncaphillis
  • 1,829
  • 10
  • 14
0

Well, it depends if you consider moving a reassigning or not. You can move a thread but not make a copy of it.

Below code will create new pair of threads each iteration and move them in place of old threads. I imagine this should work, because new thread objects will be temporaries.

while(user doesn't interrupt)
{
//Process first batch of data while acquiring new data
std::thread proc1(ProcessData,memoryAddress1a);
std::thread proc2(ProcessData,memoryAddress2a);
acq1 = std::thread(AcquireData, boardHandle1, memoryAddress1b);
acq2 = std::thread(AcquireData, boardHandle2, memoryAddress2b);
acq1.join();
acq2.join();
proc1.join();
proc2.join();
/*Proceed in this manner, alternating which memory address 
is written to and being processed until the user interrupts the program.*/
}

What's going on is, the object actually does not end it's lifetime at the end of the iteration, because it is declared in the outer scope in regard to the loop. But a new object gets created each time and move takes place. I don't see what can be spared (I might be stupid), so I imagine this it's exactly the same as declaring acqs inside the loop and simply reusing the symbol. All in all ... yea, it's about how you classify a create temporary and move.

Also, this clearly starts a new thread each loop (of course ending the previously assigned thread), it doesn't make a thread wait for new data and magically feed it to the processing pipe. You would need to implement it a differently like. E.g: Worker threads pool and communication over queues.

References: operator=, (ctor).

I think the errors you get are self-explanatory, so I'll skip explaining them.

luk32
  • 15,002
  • 33
  • 58
  • Thank you, I tried this and it appears to work as needed. Time will tell if there are any problems with it, but for now it's helping considerably! – notaCSmajor Oct 23 '14 at 13:36
-1

I think you need a much more simpler answer for running a set of threads more than once, this is the best solution:

do{

    std::vector<std::thread> thread_vector;

     for (int i=0;i<nworkers;i++)
     {
       thread_vector.push_back(std::thread(yourFunction,Parameter1,Parameter2, ...));
    }

    for(std::thread& it: thread_vector)
    { 
      it.join();
    }
   q++;
} while(q<NTIMES);
HDJEMAI
  • 7,766
  • 41
  • 60
  • 81
-1

You also could make your own Thread class and call its run method like:

class MyThread
{
public:
void run(std::function<void()> func) {
   thread_ = std::thread(func);
}
void join() {
   if(thread_.joinable())
      thread_.join();
}
private:
   std::thread thread_;
};

// Application code...
MyThread myThread;
myThread.run(AcquireData);
Christoph
  • 79
  • 2