简体   繁体   English

c ++线程池:用于将函数/ lambdas传递给线程的std :: function的替代方法?

[英]c++ thread pool: alternative to std::function for passing functions/lambdas to threads?

I have a thread pool that I use to execute many tiny jobs (millions of jobs, dozens/hundreds of milliseconds each). 我有一个线程池,我用它来执行许多小工作(数百万个工作,每个几十个/几百毫秒)。 The jobs are passed in the form of either: 工作以以下任何一种形式传递:

std::bind(&fn, arg1, arg2, arg3...)

or 要么

[&](){fn(arg1, arg2, arg3...);}

with the thread pool taking them like this: 使用线程池将它们像这样:

std::queue<std::function<void(void)>> queue;

void addJob(std::function<void(void)> fn)
{
    queue.emplace_back(std::move(fn));
}

Pretty standard stuff....except that I've noticed a bottleneck where if jobs execute in a fast enough time (less than a millisecond), the conversion from lambda/binder to std::function in the addJob function actually takes longer than execution of the jobs themselves. 非常标准的东西....除了我注意到瓶颈,如果作业在足够快的时间内执行(小于一毫秒),在addJob函数中从lambda / binder到std :: function的转换实际上需要比执行工作本身。 After doing some reading, std::function is notoriously slow and so my bottleneck isn't necessarily unexpected. 在做了一些阅读之后,std :: function非常慢,因此我的瓶颈并不一定是出乎意料的。

Is there a faster way of doing this type of thing? 有没有更快的方式来做这种事情? I've looked into drop-in std::function replacements but they either weren't compatible with my compiler or weren't faster. 我已经研究了drop-in std :: function替换,但它们或者与我的编译器不兼容或者速度不快。 I've also looked into "fast delegates" by Don Clugston but they don't seem to allow the passing of arguments along with functions (maybe I don't understand them correctly?). 我也研究了Don Clugston的“快速代表”,但他们似乎不允许将参数与函数一起传递(也许我不能正确理解它们?)。

I'm compiling with VS2015u3, and the functions passed to the jobs are all static, with their arguments being either ints/floats or pointers to other objects. 我正在使用VS2015u3进行编译,传递给作业的函数都是静态的,它们的参数是ints / floats或指向其他对象的指针。

Have a separate queue for each of the task types - you probably don't have tens of thousands of task types . 为每个任务类型都有一个单独的队列 - 您可能没有成千上万的任务类型 Each of these can be eg a static member of your tasks. 这些中的每一个都可以是例如任务的静态成员。 Then addJob() is actually the ctor of Task and it's perfectly type-safe. 然后addJob()实际上是Task的ctor,它是完全类型安全的。

Then define a compile-time list of your task types and visit it via template metaprogramming (for_each). 然后定义任务类型的编译时列表,并通过模板元编程(for_each)访问它。 It'll be way faster as you don't need any virtual call fnptr / std::function<> to achieve this. 它会更快,因为你不需要任何虚拟调用fnptr / std::function<>来实现这一点。

This will only work if your tuple code sees all the Task classes (so you can't eg add a new descendant of Task to an already running executable by loading the image from disc - hope that's a non-issue). 这只有在您的元组代码看到所有Task类时才会起作用(因此您不能通过从光盘加载图像来向已经运行的可执行文件添加Task的新后代 - 希望这不是问题)。

template<typename D> // CRTP on D
class Task {
public:
    // you might want to static_assert at some point that D is in TaskTypeList

    Task() : it_(tasks_.end()) {} // call enqueue() in descendant

    ~Task() {
        // add your favorite lock here
        if (queued()) {
            tasks_.erase(it_);
        }
    }

    bool queued() const { return it_ != tasks_.end(); }

    static size_t ExecNext() {
        if (!tasks_.empty()) {
            // add your favorite lock here
            auto&& itTask = tasks_.begin();
            tasks_.pop_front();
            // release lock
            (*itTask)();
            itTask->it_ = tasks_.end();
        }
        return tasks_.size();
    }

protected:
    void enqueue() const
    {
        // add your favorite lock here
        tasks_.push_back(static_cast<D*>(this));
        it_ = tasks_.rbegin();
    }

private:
    std::list<D*>::iterator it_;

    static std::list<D*> tasks_; // you can have one per thread, too - then you don't need locking, but tasks are assigned to threads statically
};

struct MyTask : Task<MyTask> {
    MyTask() { enqueue(); } // call enqueue only when the class is ready
    void operator()() { /* add task here */ }
    // ...
};

struct MyTask2; // etc.

template<typename...>
struct list_ {};

using TaskTypeList = list_<MyTask, MyTask2>;

void thread_pocess(list_<>) {}

template<typename TaskType, typename... TaskTypes>
void thread_pocess(list_<TaskType, TaskTypes...>)
{
    TaskType::ExecNext();
    thread_process(list_<TaskTypes...>());
}

void thread_process(void*)
{
    for (;;) {
        thread_process(TaskTypeList());
    }
}

There's a lot to tune on this code: different threads should start from different parts of the queue (or one would use a ring, or several queues and either static/dynamic assignment to threads), you'd send it to sleep when there are absolutely no tasks, one could have an enum for the tasks, etc. 调整这段代码有很多东西:不同的线程应该从队列的不同部分开始(或者一个人会使用一个环,或者几个队列以及静态/动态分配给线程),当有空的时候你会把它发送到睡眠状态。绝对没有任务,可以有任务的枚举等。

Note that this can't be used with arbitrary lambdas: you need to list task types. 请注意,这不能与任意lambdas一起使用:您需要列出任务类型。 You need to 'communicate' the lambda type out of the function where you declare it (eg by returning `std::make_pair(retval, list_) and sometimes it's not easy. However, you can always convert a lambda to a functor, which is straightforward - just ugly. 你需要在声明它的函数中“传递”lambda类型(例如,通过返回`std :: make_pair(retval,list_),有时候这并不容易。但是,你总是可以将lambda转换为functor,很简单 - 只是丑陋。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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