【问题标题】:Generic thread pool class is not working properly通用线程池类无法正常工作
【发布时间】:2020-10-15 06:45:52
【问题描述】:

我正在尝试创建一个线程池类,它接收多个函数并将它们放入队列中直到它们完成,然后我可以添加另一个函数来利用创建的线程,而不是在我想运行其他函数时创建它们.这就是为什么我包含一个条件变量来同步所有线程。

但是,代码无法正常工作,因为在调用函数时,对象会以某种方式进行复制。经过几次尝试,我无法弄清楚我错过了什么!

我期望的是hw 对象的成员函数greetings 与他的索引并行执行。但是当执行(o.*f)(std::forward<Args>(args)...); 行时,对象被复制,尽管复制构造函数被删除。因此,当它进入greetings 成员时,它会产生一个SEGMENTATION FAULT

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)

project(boost_asyo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(boost_asyo main.cpp)
target_link_libraries(${PROJECT_NAME} boost_thread boost_system)

main.cpp

#include <iostream>
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <memory>

#include <mutex>
#include <condition_variable>

class Semaphore
{
    std::mutex lock;

    std::condition_variable cond;

    int count;

public:

    Semaphore()
    {
        count = 0;
    }

    void wait()
    {
        std::unique_lock<std::mutex> m(lock);

        while(count > 0)
            cond.wait(m, [this]{ return count == 0; });
    }

    void take()
    {
        std::unique_lock m(lock);

        count++;
    }

    void give()
    {
        std::unique_lock m(lock);

        count--;

        if(count == 0)
        {
            cond.notify_one();
        }
    }
};


class ThreadPool
{
private:
    boost::asio::io_service m_io_service;
    std::unique_ptr<boost::asio::io_service::work> m_work;
    boost::thread_group m_threads;
    Semaphore m_sem;

public:
    ThreadPool(size_t n)
    {
        this->m_work = std::make_unique<boost::asio::io_service::work>(m_io_service);

        for (size_t ii = 0; ii < n; ii++)
        {
            m_threads.create_thread(boost::bind(&boost::asio::io_service::run, &this->m_io_service));
        }
    }

    ThreadPool(const ThreadPool & v) = delete;
    ThreadPool(ThreadPool && v) = delete;

    ~ThreadPool()
    {
        m_io_service.stop();
    }

    template<class type, class T, class T1, class... Args>
    auto post(type T::*f, T1 &obj, Args... args)
    {
        this->m_sem.take();
        this->m_io_service.post([&] ()
        {
            T o = static_cast<T&&>(obj);

            (o.*f)(std::forward<Args>(args)...);
            this->m_sem.give();
        });

    }

    void wait()
    {
        this->m_sem.wait();
    }
};

class HelloWorld
{
private:

public:
    std::string m_str;
    HelloWorld(std::string str) : m_str(str) {};
    HelloWorld(const HelloWorld& v) = delete;
    HelloWorld(HelloWorld&& v) = default;

    ~HelloWorld() = default;

    void greetings(int ii)
    {
        for (int jj = 0; jj < 5; jj++)
        {
            std::cout << this->m_str << " " << ii <<  std::endl;

            boost::this_thread::sleep_for(boost::chrono::seconds(1));
        }

    }
};


int main()
{
    ThreadPool tp(8);

    HelloWorld hw("Hola mundo");

    for (int ii = 0; ii < 5; ii++)
    {
        tp.post(&HelloWorld::greetings, hw, ii);
    }

    tp.wait();

    return 0;
}

此代码基于此代码,它可以正常工作,这与我想要对类和成员执行的操作类似。

#include <iostream>
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <memory>

#include <mutex>
#include <condition_variable>

class Semaphore
{
    std::mutex lock;

    std::condition_variable cond;

    int count;

public:

    Semaphore()
    {
        count = 0;
    }

    void wait()
    {
        std::unique_lock<std::mutex> m(lock);

        while(count > 0)
            cond.wait(m, [this]{ return count == 0; });
    }

    void take()
    {
        std::unique_lock m(lock);

        count++;
    }

    void give()
    {
        std::unique_lock m(lock);

        count--;

        if(count == 0)
        {
            cond.notify_one();
        }
    }
};


int main()
{    
    boost::asio::io_service io_service;
    std::unique_ptr<boost::asio::io_service::work> work = std::make_unique<boost::asio::io_service::work>(io_service);

    boost::thread_group threads;

    for (size_t ii = 0; ii < 2; ii++)
    {
        std::cout << "id: " << ii << std::endl;
        threads.create_thread(boost::bind(&boost::asio::io_service::run, &io_service));
    }

    Semaphore sem;

    for (size_t ii = 0; ii < 3; ii++)
    {
        //Take
        sem.take();

        io_service.post([ii, &sem] ()
        {
            int id = 0;
            while(id < 5)
            {
                id++;
                printf("hello world %i\n", static_cast<int>(ii));
                boost::this_thread::sleep_for(boost::chrono::seconds(1));
            }

            //Give
            sem.give();
        });
    }


    sem.wait();


    for (size_t ii = 0; ii < 3; ii++)
    {
        sem.take();

        io_service.post([ii, &sem] ()
        {
            int id = 0;
            while(id < 5)
            {
                id++;
                printf("bye world %i\n", static_cast<int>(ii));
                boost::this_thread::sleep_for(boost::chrono::seconds(1));
            }
            sem.give();
        });
    }

    sem.wait();

    io_service.stop();

    return 0;
}


【问题讨论】:

  • 什么函数可以复制?什么是输出/预期输出?
  • 是的,你是对的。我编辑了我的问题以提供更多详细信息。我期望的是成员函数与线程池并行运行

标签: c++ multithreading boost lambda threadpool


【解决方案1】:

我真的很好奇信号量是关于什么的。

io_service 已经是一个任务队列。它是线程安全的,您不需要信号量。

为了比较,这里是基于 io_service 的线程池:

(更好的是,最近的 Asio 版本有一个内置的线程池)。

哪里出错了

这是不安全的:

template <class type, class T, class T1, class... Args>
auto post(type T::*f, T1& obj, Args... args) {
    this->m_sem.take();
    this->m_io_service.post([&]() {
        T o = static_cast<T&&>(obj);
        (o.*f)(std::forward<Args>(args)...);
        this->m_sem.give();
    });
}

具体来说:

  1. 线

    T o = static_cast<T&&>(obj);
    

    不复制 T(即HelloWorld)。你知道那是因为那是不可能的。更糟糕的是:对象从obj 移出。

    顺便说一句,这假设 T 可以从 T1 移动构造。

    您通过将右侧显式转换为右值引用来明确要求它。

    这就是 std::move 指定要做的事情,实际上:"In particular, std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to a static_cast to an rvalue reference type."

    效果是 main 中的 HelloWorld 实例不再有效,但您继续从它移开以执行后续任务。

  2. 通过引用捕获的其他参数。这意味着它们在任务实际执行之前就超出了范围(包括f)。

为了确保安全,您必须在本地副本中捕获参数:

template <class type, class T, class... Args>
auto post(type T::*f, T&& obj, Args... args) {
    this->m_sem.take();
    this->m_io_service.post([=, o = std::move(obj)]() mutable {
        try {
            (o.*f)(args...);
        } catch (...) {
            this->m_sem.give();
            throw;
        }
        this->m_sem.give();
    });
}

注意事项:

  1. 现在objrvalue 引用。这意味着除非obj 是右值,否则post 不会编译。

    注意这不是universal reference,因为Tf 的一部分。

  2. lambda 现在是可变的(因为否则只有 const 成员函数可以在捕获的 o 上运行)

  3. 所有其他参数都被复制 - 这大致是std::bind 的操作方式,但您可以针对可移动参数进行优化)。

  4. 我们处理异常 - 在您的代码中,如果 f 抛出,您将永远不会 give() 信号量

当然main需要适配所以多个HelloWorld实例实际上是通过右值创建和传递的:

for (int ii = 0; ii < 5; ii++) {
    HelloWorld hw("Hola mundo");
    tp.post(&HelloWorld::greetings, std::move(hw), ii);
}

但是 - 它不会工作

至少,对我来说它不会编译。 Asio 要求处理程序是可复制的(why must a Boost.Asio handler be copy constructible?How to trick boost::asio to allow move-only handlers)。

此外,我们几乎没有触及表面。通过对type T::*f 进行硬编码,您做到了,因此您需要新的post 重载来处理许多事情:静态方法、const 成员函数...

相反,为什么不使用 C++ 方式:

template <class F, class... Args>
auto post(F&& f, Args&&... args) {
    this->m_sem.take();
    this->m_io_service.post(
        [this, f=std::bind(std::forward<F>(f), std::forward<Args>(args)...)] 
        {
            try { f(); }
            catch (...) {
                this->m_sem.give();
                throw;
            }
            this->m_sem.give();
        });
}

实际上,在更现代的 C++ 中,你会写(假设这里是 c++17):

    //...
        [this, f=std::forward<F>(f), args=std::make_tuple(std::forward<Args>(args)...)] 
        {
            try { std::apply(f, args); }
    //...

哦,我们仍然需要

#define BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS 1

因为只移动处理程序类型

完整的固定版本演示

注意:还添加了一个输出互斥锁 (s_outputmx) 以避免混合控制台输出。

Live On Coliru

#define BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS 1
#include <iostream>
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <memory>

#include <mutex>
#include <condition_variable>

class Semaphore {
    std::mutex lock;
    std::condition_variable cond;
    int count;
  public:
    Semaphore() { count = 0; }
    void wait() {
        std::unique_lock<std::mutex> m(lock);
        while (count > 0)
            cond.wait(m, [this] { return count == 0; });
    }
    void take() {
        std::unique_lock m(lock);
        count++;
    }
    void give() {
        std::unique_lock m(lock);
        count--;
        if (count == 0) {
            cond.notify_one();
        }
    }
};


class ThreadPool {
  private:
    boost::asio::io_service m_io_service;
    std::unique_ptr<boost::asio::io_service::work> m_work;
    boost::thread_group m_threads;
    Semaphore m_sem;

  public:
    ThreadPool(size_t n) {
        this->m_work =
            std::make_unique<boost::asio::io_service::work>(m_io_service);
        for (size_t ii = 0; ii < n; ii++) {
            m_threads.create_thread(boost::bind(&boost::asio::io_service::run,
                                                &this->m_io_service));
        }
    }
    ThreadPool(const ThreadPool& v) = delete;
    ThreadPool(ThreadPool&& v) = delete;
    ~ThreadPool() { m_io_service.stop(); }

    template <class F, class... Args>
    auto post(F&& f, Args&&... args) {
        this->m_sem.take();
        this->m_io_service.post(
#if 1 // pre-c++17
            [this, f=std::bind(std::forward<F>(f), std::forward<Args>(args)...)] 
            {
                try { f(); }
#else // https://en.cppreference.com/w/cpp/utility/apply
            [this, f=std::forward<F>(f), args=std::make_tuple(std::forward<Args>(args)...)] 
            {
                try { std::apply(f, args); }
#endif
                catch (...) {
                    this->m_sem.give();
                    throw;
                }
                this->m_sem.give();
            });
    }


    void wait() { this->m_sem.wait(); }
};

struct HelloWorld {
    std::string m_str;

    HelloWorld(std::string str) : m_str(str){};
    HelloWorld(const HelloWorld& v) = delete;
    HelloWorld(HelloWorld&& v) = default;
    ~HelloWorld() = default;

    void greetings(int ii) const {
        for (int jj = 0; jj < 5; jj++) {
            {
                static std::mutex s_outputmx;
                std::lock_guard<std::mutex> lk(s_outputmx);
                std::cout << this->m_str << " " << ii << std::endl;
            }
            boost::this_thread::sleep_for(boost::chrono::seconds(1));
        }
    }
};

int main()
{
    ThreadPool tp(8);

    for (int ii = 0; ii < 5; ii++) {
        HelloWorld hw("Hola mundo");
        tp.post(&HelloWorld::greetings, std::move(hw), ii);
    }

    tp.wait();
}

打印

Hola mundo 0
Hola mundo 2
Hola mundo 3
Hola mundo 1
Hola mundo 4
Hola mundo 0
Hola mundo 1
Hola mundo 4
Hola mundo 2
Hola mundo 3
Hola mundo 0
Hola mundo 1
Hola mundo 4
Hola mundo 2
Hola mundo 3
Hola mundo 0
Hola mundo 4
Hola mundo 2
Hola mundo 3
Hola mundo 1
Hola mundo 0
Hola mundo 4
Hola mundo 2
Hola mundo 1
Hola mundo 3

奖励:丢弃信号量

删除信号量并实际使用work

class ThreadPool {
    boost::asio::io_service m_io_service;
    std::unique_ptr<boost::asio::io_service::work> m_work;
    boost::thread_group m_threads;

  public:
    ThreadPool(size_t n)
        : m_work(std::make_unique<boost::asio::io_service::work>(m_io_service))
    {
        while (n--) {
            m_threads.create_thread([this] { m_io_service.run(); });
        }
    }

    ~ThreadPool() { wait(); }

    void wait() {
        m_work.reset();
        m_threads.join_all();
    }

    template <class F, class... Args> void post(F&& f, Args&&... args) {
        m_io_service.post(
            [f=std::forward<F>(f), args=std::make_tuple(std::forward<Args>(args)...)] {
                std::apply(f, args); 
            });
    }
};

这是 28 行代码,而您的原始代码为 90 行。它实际上做了更多的事情

也可以看到 Live On Coliru

还剩下什么?

我们没有正确处理来自io_service::run 的异常(请参阅Should the exception thrown by boost::asio::io_service::run() be caught?

另外,如果你有“最近的”Boost,你可以享受到work的改进界面(make_work_guard.reset()所以你不需要unique_ptr),以及一个现成的thread_pool (所以你不需要......基本上任何东西了):

Live On Coliru

#include <boost/asio.hpp>
#include <mutex>
#include <iostream>
static std::mutex s_outputmx;
using namespace std::chrono_literals;

struct HelloWorld {
    std::string const m_str;
    void greetings(int ii) const;
};

int main() {
    boost::asio::thread_pool tp(8);

    for (int ii = 0; ii < 5; ii++)
        //post(tp, [hw=HelloWorld{"Hola mundo"}, ii] { hw.greetings(ii); });
        post(tp, std::bind(&HelloWorld::greetings, HelloWorld{"Hola mundo"}, ii));

    tp.join();
}

void HelloWorld::greetings(int ii) const {
    for (int jj = 0; jj < 5; jj++) {
        std::this_thread::sleep_for(1s);

        std::lock_guard<std::mutex> lk(s_outputmx);
        std::cout << m_str << " " << ii << std::endl;
    }
}

【讨论】:

  • 添加了一些奖励和现场演示,包括修复所有问题并适合整个事情的一个
  • 非常感谢@sehe。我很欣赏你的回答,它非常有用。信号量类旨在在不停止“io_service”的情况下同步线程,以便以后使用它们。还有一个问题,在您的上一个脚本中,我无法再向 thread_pool 发布其他功能。调用tp.join()后有没有办法复用thread_pool?
猜你喜欢
  • 2016-02-25
  • 2014-03-05
  • 1970-01-01
  • 1970-01-01
  • 2013-07-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多