简体   繁体   中英

comunication between threads using condition variables

I am trying to implement an algorithm for process histograms images using multiple threads.

One of the most common approach is to split multiple thread create a cache buffer on each one, do the histogram on the cache buffer and then lock a mutex addition the values on the local to the output vector, and unlock the buffer.

This approach is very efficient but can introduce a 'jam'. I mean the additions of the datas cannot be realized concurently.

In most of the case when the range of the values is quite short (eg 0-255) it's the time needed to proceed the addition is very fast and can be neglect.

If the range of datas is higher like for example on thermal images this time can become more significant. Thermal images are often matrix of unsigned short, even if the values doesn't use the full range (0-65535) the algorithm must process all the range.

In order to speed up a little bit hte processing I thought to launch a background thread to do the addition while the "foreground" thread would only write the datas into a preallocated buffer.

So basically the work of a "foreground" thread used to be that :

  • get a buffer from a circular buffer.

  • process the histogram for a specified set of data (eg from line n to line m).

-notify the background buffer the operations are finished.

The background thread used to do :

  • wait until a notification arrive and check if the number of available buffer is lower than the number of buffers.

  • If the conditions are true then look for the buffer to be processed from the buffers available.

  • Do the addition with the output buffer.

  • make the processed buffer reusable.

I am not very familiar with condition variables. So in order to check communication between the threads using conditions variable I wrote the following toy sample :

toy.h

#ifndef TOY
#define TOY

#include <opencv2/core.hpp>
#include <opencv2/core/utility.hpp>

#include <iostream>
#include <iterator>

#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>


namespace toy
{

class toy_t : public cv::ParallelLoopBody
{
private:

    struct elem_t
    {
        int _id;
        bool _is_available;
        bool _is_inprocess;

        inline elem_t():
            _id(-1),
            _is_available(true),
            _is_inprocess(false)
        {}
        // help for the initialization using iota.
        inline elem_t& operator=(const int& id)
        {
            this->_id = id;

            return (*this);
        }
    };

    const int _nb_thread_available;

    std::vector<elem_t> buf;
    elem_t* buf_begin;
    elem_t* buf_end;

    mutable std::atomic_size_t _nb_buffer_available;

    std::atomic_bool _run;
    std::atomic_bool _is_background_terminate;


    mutable std::mutex _mtx_fgd;
    mutable std::mutex _mtx_bgd;

    mutable std::condition_variable _cv_foreground;
    mutable std::condition_variable _cv_background;

    std::condition_variable _cv_thread;

    elem_t* get_buffer()const
    {
        // Wait untill a conditionnal variable notify that a buffer is ready to be reused.
        std::unique_lock<std::mutex> lck(this->_mtx_fgd);

        this->_cv_foreground.wait(lck,[&]{ return (this->_nb_buffer_available > 0);});

        elem_t* it = this->buf_begin;

        // Look for available buffer.

        while(!it->_is_available )
            it++;

        it->_is_available = false;
        it->_is_inprocess = true;

        this->_nb_buffer_available--;

        return it;

    }

    void background()
    {
        std::cout<<"background launch "<<std::endl;
        while(this->_run)
        {
            std::unique_lock<std::mutex> lck(this->_mtx_bgd);

            // Wait for a notification.
            this->_cv_background.wait(lck,[&]{return (this->_nb_buffer_available != cv::getNumThreads()) ;});

            //
            if(!this->_run)
                continue;

            elem_t* it = this->buf_begin;

            // Method by spining.
            // While the available buffer is not find I am looking for it.
            // When I'll find I may have done multiple pass.
            while(it->_is_available || it->_is_inprocess)
            {
                it++;

                if(it == this->buf_end)
                    it = this->buf_begin;
            }

            // This method is more logic than the spinner.
            // A condition variable has notify a buffer is ready to be reused, so a one pass check is made in order to find which is this buffer.
    //        while(!it->_is_available )
    //            it++;


            std::cout<<"the background thread is making the buffer : "<<it->_id<<" availlable."<<std::endl;

            // Do something.

            it->_is_available = true;
            it->_is_inprocess = false;

            this->_nb_buffer_available++;

            this->_cv_foreground.notify_one();
        }

        this->_is_background_terminate = true;
    }

public:

    toy_t():
        _nb_thread_available(cv::getNumThreads()), // In my computer getNumThreads() == 8
        buf(),
        buf_begin(nullptr),
        buf_end(nullptr),
        _nb_buffer_available(this->_nb_thread_available),
        _run(false),
        _is_background_terminate(false)
    {

        this->buf.reserve(this->_nb_buffer_available);
        this->buf.resize(this->buf.capacity());

        std::iota(this->buf.begin(),this->buf.end(),0);

        this->buf_begin = this->buf.data();
        this->buf_end = this->buf_begin + this->buf.size();

        std::thread th([this]{ this->_cv_thread.notify_one(); this->background();});

        this->_run = true;

        th.detach();

    }

    virtual ~toy_t()
    {
        this->_run = false;

        this->_nb_buffer_available = 0;
        this->_cv_background.notify_one();

        while(!this->_is_background_terminate)
            std::this_thread::yield();
    }

    // foreground threads
    virtual void operator()(const cv::Range& range)const
    {
            elem_t* it = this->get_buffer();

            std::cout<<"the foreground thread is processing the buffer : "<<it->_id<<std::endl;

            for(int r=range.start;r<range.end;r++)
            {
                // Do something.
            }

            std::this_thread::sleep_for(std::chrono::seconds(1));

            it->_is_inprocess = false;


            this->_cv_background.notify_one();
    }


};

}

#endif // TOY

main .cpp

#include <iostream>
#include <cstdlib>

#include "toy.h"





int main(int argc,char* argv[])
{


        toy::toy_t tt;

        cv::parallel_for_(cv::Range(0,15),tt);




    std::cout << "Hello World!" << std::endl;
    return EXIT_SUCCESS;
}

This code as is use to work without difficulties.

The unwanted aspect of the code is written on the method background :

    // Method by spining.
    // While the available buffer is not find I am looking for it.
    // When I'll find I may have done multiple pass.
    while(it->_is_available || it->_is_inprocess)
    {
        it++;

        if(it == this->buf_end)
            it = this->buf_begin;
    }

I must check the position of the variable "it" otherwise it can take a position outside of the buffer size.

The way I thought was : - At the end of the foreground thread a notification is send to the background thread. - Then the background thread process the buffers (or the buffers depending how fast the threads are ending). - At the end the background thread notify to the next foreground thread (in the method get_buffer()) it has finish to process the buffer and has made it reusable.

Following these statement when the background thread is looking for a thread it use to find it between Buf_start and _Buf_end.

So the seeking of the buffer in the method background use to be :

while(!it->_is_available )
    it++;

After several hours of tests I have no idea what's wrong. I am also interested to know if this algorithm really work like I think it do ? Is there any more efficient, less processing way to communicate between threads ?

Thanks in advance.

I fix it! :).

I identify several issues from the class "toy".

In the overload of the operator () I copy past the code of the method get buffer() . I did a block function around the wait condition in order to not to be sequential over thread. The unique_lock needed by the condition variable exist only between the brackets of the block function, like this when the wait condition has been evaluated the mutex _mtx_foreground is unlock.

Then in order to prevent an outbound during the search of the available buffer the while loop has been replaced by a for loop. The issue was on the condition to satisfy for increment the pointer.

On the "background" thread I add a loop. The idea is that :

  1. The background thread must run until the end of the end of the algorithm.
  2. The background thread is waiting for a notification from one of the foreground thread.
  3. Look for a buffer to process, process it, remake it available and notify it to the foreground thread.
  4. redo until all the buffers are processed then go back to 2).

Here some part have been "deeply" modified.

The up to date implementation of the class toy called toy2 is this one :

#ifndef TOY
#define TOY


#include <opencv2/core.hpp>
#include <opencv2/core/utility.hpp>

#include <iostream>
#include <iterator>

#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>

namespace toy
{

class toy2 : public cv::ParallelLoopBody
{

private:

    struct elem_t
    {
        int _id;
        std::atomic_bool _is_available;
        std::atomic_bool _is_inprocess;

        inline elem_t():
            _id(-1),
            _is_available(true),
            _is_inprocess(false)
        {}

        // needed for the memory reservation because atomic_bool copy constructor is deleted.
        inline elem_t(const elem_t& obj):
            _id(obj._id),
            _is_available((bool)obj._is_available),
            _is_inprocess((bool)obj._is_inprocess)
        {}

        // needed for the memory reservation because atomic_bool copy constructor is deleted.
        inline elem_t(elem_t&& obj):
            _id(obj._id),
            _is_available((bool)obj._is_available),
            _is_inprocess((bool)obj._is_inprocess)
        {}

        // help for the initialization using iota.
        inline elem_t& operator=(const int& id)
        {
            this->_id = id;

            return (*this);
        }
    };

    mutable std::vector<elem_t> _buffer;
    std::vector<elem_t*> _elements;

    std::atomic_bool _run;

    mutable std::atomic_size_t _nb_available_buffers;

    mutable std::mutex _mtx_thread;
    mutable std::mutex _mtx_foreground;
    mutable std::mutex _mtx_background;

    mutable std::condition_variable _cv_ctor_dtor;
    mutable std::condition_variable _cv_foreground;
    mutable std::condition_variable _cv_background;



    void background()
    {
        std::cout<<"background has been detach"<<std::endl;


        while(this->_run)
        {
            {
            std::unique_lock<std::mutex> lck(this->_mtx_background);

            this->_cv_background.wait(lck);
            }

            // Condition for stoping terminate the thread.
            if(!this->_run)
                break;

            while(true)
            {

                typename std::vector<elem_t>::iterator it = std::find_if(this->_buffer.begin(),this->_buffer.end(),[](const elem_t& v){ return (!v._is_available && !v._is_inprocess);});

                if(it == this->_buffer.end())
                    break;

                std::cout<<"the background is making the element : "<<it->_id<<" available."<<std::endl;

                it->_is_available = true;
                it->_is_inprocess = false;

                this->_nb_available_buffers++;

                this->_cv_foreground.notify_one();
            }


        }
    }

public:


    toy2():
        _buffer(),
        _elements(),
        _run(false),
        _nb_available_buffers(0),
        _mtx_thread(),
        _mtx_foreground(),
        _mtx_background(),
        _cv_ctor_dtor(),
        _cv_foreground(),
        _cv_background()
    {
        const int nb_threads = cv::getNumThreads();

        this->_nb_available_buffers = nb_threads;

        this->_buffer.reserve(nb_threads);
        this->_buffer.resize(this->_buffer.capacity());

        this->_elements.reserve(this->_buffer.size());
        this->_elements.resize(this->_buffer.size(),nullptr);



        std::iota(this->_buffer.begin(),this->_buffer.end(),0);

        for(int i=0;i<this->_buffer.size();i++)
            this->_elements[i] = std::addressof(this->_buffer[i]);

        std::thread th([this]
        {
            // Notify to the constructor.
            this->_cv_ctor_dtor.notify_one();

            this->background();

            // Notify to the destructor.
            this->_cv_ctor_dtor.notify_one();
        });

        this->_run = true;

        std::unique_lock<std::mutex> lck(this->_mtx_thread);

        th.detach();

        this->_cv_ctor_dtor.wait(lck);

    }


    ~toy2()
    {
        this->_run = false;
        this->_cv_background.notify_one();

        std::unique_lock<std::mutex> lck(this->_mtx_thread);

        this->_cv_ctor_dtor.wait(lck);
    }

    void operator()(const cv::Range& range)const
    {
        {
            std::unique_lock<std::mutex> lck(this->_mtx_foreground);

            this->_cv_foreground.wait(lck,[&]{return this->_nb_available_buffers>0;});
        }

        typename std::vector<elem_t>::iterator it = std::find_if(this->_buffer.begin(),this->_buffer.end(),[](const elem_t& v){return (bool)v._is_available;});

//        for(it = this->_buffer.begin();it != this->_buffer.end();it++)
//            if(it->_is_available)
//                break;



        it->_is_available = false;
        it->_is_inprocess = true;

        this->_nb_available_buffers--;

        std::cout<<"the foreground is processing the element : "<<it->_id<<" "<<std::this_thread::get_id()<<std::endl;




        std::this_thread::sleep_for(std::chrono::milliseconds(2));
//        std::this_thread::sleep_for(std::chrono::seconds(2));

        it->_is_inprocess = false;

        this->_cv_background.notify_one();

        std::cout<<"end thread"<<std::endl;
    }

};

    }
#endif

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM