【问题标题】:boost thread worker object re-use after thread has finished线程完成后提升线程工作者对象的重用
【发布时间】:2016-02-23 05:29:34
【问题描述】:

我有一个简单的线程对象,它负责一些执行(工作人员):

在最简单的形式中,为每个线程创建一个对象:

class worker
{
public:

    worker  (
              boost::atomic<int> & threads,
              boost::mutex & mutex,
              boost::condition_variable & condition
            )
    : threads__(threads), mutex__(mutex), condition__(condition)
    {}  

    void run (
                 // some params
             )
    {   
        // ... do the threaded work here
        // finally, decrease number of running threads and notify
        boost::mutex::scoped_lock lock(mutex__);
        threads__--;
        condition__.notify_one();
    }   

private:
    boost::atomic<int> & threads__;
    boost::mutex & mutex__;
    boost::condition_variable & condition__;
};

我使用它的方式是在一个循环中运行最多 8 个并发线程,如果一个线程完成则等待通知,以便生成下一个线程:

boost::thread_group thread_group;
boost::mutex mutex;
boost::condition_variable condition;
boost::atomic<int> threads(0);

// Some loop which can be parallelised
for ( const auto & x : list )
{
    // wait if thread_count exceeds 8 threads
    boost::mutex::scoped_lock lock(mutex);
    while ( threads >= 8 ) 
         condition.wait( lock );

    // Create worker object
    worker _wrk_( threads, mutex, condition );

    boost::thread * thread = new boost::thread( &worker::run, &_wrk_, /* other params */ );
    thread_group.add_thread( thread );
    threads++;
}

这适用于我的大多数场景,但现在我有一个需要重复使用的线程对象。

原因很简单:这个tread 对象包含thrust::device_vector&lt;float&gt;,当对象被删除时(重新)分配的代价很高。

此外,这些向量可以重复使用,因为它们的大部分内容不会改变。

因此,我正在寻找一种可以重用在循环中创建的对象的机制——事实上,我将预先分配 8 个这些对象(或与我的并发线程一样多),然后再使用它们一遍又一遍。 我希望可以做到的事情是这样的:

boost::thread_group thread_group;
boost::mutex mutex;
boost::condition_variable condition;
boost::atomic<int> threads(0);

// our worker objects to be reused
std::vector<std::shared_ptr<worker>>workers(8,std::make_shared<worker>(threads,mutex,condition));

// Some loop which can be parallelised
for ( const auto & x : list )
{
    // wait if thread_count exceeds 8 threads
    boost::mutex::scoped_lock lock(mutex);
    while ( threads >= 8 ) 
         condition.wait( lock );

    // get next available thread object from the vector
    auto _wrk_ = std::find_if(workers.begin(), workers.end(), is_available() );

    // if we have less than 8 threads but no available thread object
    if ( _wrk_ == workers.end() ) throw std::runtime_error ("...");

    // Use the first available worker object for this thread
    boost::thread * thread = new boost::thread(&worker::run, &(*_wrk_));
    thread_group.add_thread( thread );
    threads++;
}

我不知道如何向 is_available() 发出信号,除了将它实现为(工作类的)类方法。

其次,在我看来,这无缘无故太复杂了,我确信必须有某种其他模式可以使用,它更简单和/或优雅。

【问题讨论】:

  • 为什么不使用已经可用的线程池?
  • @SergeyA 你的意思是一个提升线程组作为一个池?
  • 我不熟悉 boost::thread_group,但从您的使用来看,您似乎没有将它用作线程池 - 您正在为每个请求创建一个新线程。我的意思是使用经典线程池,当您在开始时启动预定义数量的线程时,将它们连接到消息队列并将“工作”请求发布到该队列以由第一个可用线程拾取。
  • @SergeyA 我不知道该怎么做。你是对的,我正在创建新线程。每个线程的参数都会改变,但对象保持不变。这些对象复制起来非常昂贵(它们会进行主机设备内存复制)。你能提供一个简化的例子来说明你的意思吗?我应该使用这样的线程池吗:stackoverflow.com/questions/12215395/…
  • 这对我来说看起来就像线程池。您可以谷歌线程池 - 这是一个非常广泛使用的概念,它应该非常适用于您的情况。

标签: c++ multithreading boost


【解决方案1】:

实现线程池的一个非常简单的方法是使用boost::asio

这里的完整示例,包括两种类型的任务(函数和对象)加上异常处理:

#include <iostream>
#include <vector>
#include <thread>
#include <string>
#include <chrono>
#include <random>
#include <condition_variable>
#include <boost/asio.hpp>

void emit(const char* txt, int index)
{
    static std::mutex m;
    std::lock_guard<std::mutex> guard { m };
    std::cout << txt << ' ' << index << std::endl;
}

struct worker_pool
{
    boost::asio::io_service _io_service;
    boost::asio::io_service::work _work { _io_service };

    std::vector<std::thread> _threads;

    std::condition_variable _cv;
    std::mutex _cvm;
    size_t _tasks = 0;


    void start()
    {
        for (int i = 0 ; i < 8 ; ++i) {
            _threads.emplace_back(std::bind(&worker_pool::thread_proc, this));
        }
    }

    void wait()
    {
        std::unique_lock<std::mutex> lock(_cvm);
        _cv.wait(lock, [this] { return _tasks == 0; });
    }

    void stop()
    {
        wait();
        _io_service.stop();
        for (auto& t : _threads) {
            if (t.joinable())
                t.join();
        }
        _threads.clear();

    }

    void thread_proc()
    {
        while (!_io_service.stopped())
        {
            try {
                _io_service.run();
            }
            catch(const std::exception& e)
            {
                emit(e.what(), -1);
            }
        }
    }

    void reduce() {

        std::unique_lock<std::mutex> lock(_cvm);
        if (--_tasks == 0) {
            lock.unlock();
            _cv.notify_all();
        }
    }

    template<class F>
    void submit(F&& f)
    {
        std::unique_lock<std::mutex> lock(_cvm);
        ++ _tasks;
        lock.unlock();
        _io_service.post([this, f = std::forward<F>(f)]
                         {
                             try {
                                 f();
                             }
                             catch(...)
                             {
                                 reduce();
                                 throw;
                             }
                             reduce();
                         });


    }
};



void do_some_work(int index, std::chrono::milliseconds delay)
{
    emit("starting work item ", index);
    std::this_thread::sleep_for(delay);
    emit("ending work item ", index);
}

struct some_other_work
{
    some_other_work(int index, std::chrono::milliseconds delay)
    : _index(index)
    , _delay(delay)
    {}

    void operator()() const {

        emit("starting some other work ", _index);

        if (!(_index % 7)) {
            emit("uh oh! ", _index);
            using namespace std::string_literals;
            throw std::runtime_error("uh oh thrown in "s + std::to_string(_index));
        }

        emit("ending some other work ", _index);
    }

    int _index;
    std::chrono::milliseconds _delay;
};

auto main() -> int
{
    worker_pool pool;
    pool.start();

    std::random_device rd;
    std::default_random_engine eng(rd());

    std::uniform_int_distribution<int> dist(50, 200);
    for (int i = 0 ; i < 1000 ; ++i) {
        std::chrono::milliseconds delay(dist(eng));
        pool.submit(std::bind(do_some_work, i, delay));
        pool.submit(some_other_work(i, delay));
    }

    pool.wait();
    pool.stop();

    return 0;
}

示例输出:

starting work item  0
starting some other work  0
starting work item  1
starting some other work  1
starting work item  2
starting some other work  2
starting work item  3
starting some other work  3
uh oh!  0
ending some other work  1
ending some other work  2
ending some other work  3
starting work item  4
uh oh thrown in 0 -1
starting some other work  4
starting work item  5
ending some other work  4
starting some other work  5
starting work item  6
ending some other work  5
starting some other work  6
ending some other work  6
starting work item  7
ending work item  0
starting some other work  7
uh oh!  7
uh oh thrown in 7 -1
starting work item  8
ending work item  1
starting some other work  8
ending some other work  8
starting work item  9
ending work item  5
starting some other work  9
ending some other work  9
starting work item  10
ending work item  7
starting some other work  10
ending some other work  10
starting work item  11
ending work item  4
starting some other work  11
ending some other work  11
starting work item  12
ending work item  3
starting some other work  12
ending some other work  12
starting work item  13
ending work item  10
ending work item  6
starting some other work  13
starting work item  14
ending some other work  13
starting some other work  14
uh oh!  14
uh oh thrown in 14 -1
...

【讨论】:

  • 感谢理查德的回答。在这种情况下唯一的问题是,我的 thread_pool 是否应该封装和控制我的线程工作对象?你使用函数作为线程,而我想围绕线程封装一个对象。
  • 工人亚历克斯!= 任务。工作人员等待任务,并且任务在工作人员上运行。封装是相同的(我的答案中使用了非 asio 实现)
  • @Alex 在我的示例中,worker_pool 对象是线程池,您可以通过submit() 向其提交任何异步任务。异步任务可以是任何不带参数的可调用对象,因此也可以是对象,只要它支持operator()
  • @RichardHodges 这是我不明白的:我用 printf 替换了你的 emit 函数(我没有 c++14,CUDA 6.5 无法使用它),然后打印some_other_workthis 和一些相同,一些变化。它们是如何管理的?我有8个不同的吗?如果我能弄清楚,这将需要我采取不同的方法,但我可以让它发挥作用。我假设它是start?中的循环构造函数?
  • 在我的例子中,每一个都是一个不同的对象,这通常是你想要的。如果您想在多个工作人员之间共享工作人员的实现,您只需将 some_other_work 重构为包含共享 pimpl 的句柄。
猜你喜欢
  • 1970-01-01
  • 2013-06-05
  • 1970-01-01
  • 2013-01-19
  • 2014-03-10
  • 1970-01-01
  • 2012-03-25
  • 1970-01-01
  • 2013-09-13
相关资源
最近更新 更多