将工作项添加到数组或列表的非阻塞方式

时间:2015-05-03 21:07:40

标签: c++ c++11 threadpool future lock-free

修改

我现在已经完成了我的队列(克服了下面描述的问题,等等)。对于那些感兴趣的人,可以找到here。我很高兴听到任何评论:)。请注意,队列不仅仅是一个工作项队列,而是一个模板容器,当然可以用工作项实例化。

原始

在C ++ 11和14中观察Herb Sutter's talk并发后,我对非阻塞并发感到兴奋。

但是,我还没有找到解决我认为是基本问题的解决方案。所以,如果已经在这里,请与我一起。

我的问题很简单。我正在创建一个非常简单的线程池。为了做到这一点,我在workPool类中运行了一些工作线程。我保留了workItems的清单。

如何以无锁方式添加工作项。

执行此操作的非锁定方式当然是创建互斥锁。如果您添加项目并在当前工作项完成后读取(并锁定当然)列表,则将其锁定。

然而,我不知道如何以无锁方式执行此操作。

低估了我正在创造的东西。这段代码我是为这个问题写的。它既不完整,也不错误:)

#include <thread>
#include <deque>
#include <vector>

class workPool
{
public:
    workPool(int workerCount) :
        running(1)
    {
        for (int i = workerCount; i > 0; --i)
            workers.push_back(std::thread(&workPool::doWork, this));
    }

    ~workPool()
    {
        running = 0;
    }
private:
    bool running;
    std::vector< std::thread > workers;
    std::deque< std::function<void()> > workItems;

    void doWork()
    {
        while (running)
        {
            (*workItems.begin())();
            workItems.erase(workItems.begin());
            if (!workItems.size())
                //here the thread should be paused till a new item is added
        }


    }

    void addWorkitem()
    {
        //This is my confusion. How should I do this?
    }

};

3 个答案:

答案 0 :(得分:2)

在这种具有共享资源(工作队列)的上下文中的自由锁通常会被原子和CAS循环所取代,如果你真的深入挖掘。

基本的想法是获得一个无锁的并发堆栈相当简单(编辑:虽然在我的第一篇文章中做了一个混乱可能有点欺骗性的棘手 - 更有理由欣赏一个好的lib)。为简单起见,我选择了一个堆栈,但使用队列并不需要更多。

写入堆栈:

Create a new work item.
Loop Repeatedly:
    Store the top pointer to the stack.
    Set the work item's next pointer to the top of the stack.
    Atomic: Compare and swap the top pointer with the pointer to the work item.
            If this succeeds and returns the top pointer we stored, break out
            of the loop.

从堆栈中弹出:

 Loop:
     Fetch top pointer.
     If top pointer is not null:
         Atomic: CAS top pointer with next pointer.
         If successful, break.
     Else:
         (Optional) Sleep/Yield to avoid burning cycles.

 Process the item pointed to by the previous top pointer.

现在,如果你真的很精细,你可以坚持其他工作,让线程在推或弹出失败时做,例如。

答案 1 :(得分:2)

我最近看过Herb的谈话,我相​​信他的lock-free linked list应该没问题。唯一的问题是atomic< shared_ptr<T> >尚未实施。我已经使用了atomic_*函数调用,这也是Herb在他的演讲中解释的。

在示例中,我将任务简化为int,但它可以是您想要的任何内容。

函数atomic_compare_exchange_weak有三个参数:要比较的项,期望值和期望值。它返回true或false以指示成功或失败。如果失败,预期值将更改为找到的值。

#include <memory>
#include <atomic>

// Untested code.

struct WorkItem { // Simple linked list implementation.
    int work;
    shared_ptr<WorkItem> next; // remember to use as atomic
};

class WorkList {
    shared_ptr<WorkItem> head; // remember to use as atomic
public:
    // Used by producers to add work to the list. This implementation adds
    // new items to the front (stack), but it can easily be changed to a queue.
    void push_work(int work) {
        shared_ptr<WorkItem> p(new WorkItem()); // The new item we want to add.
        p->work = work;
        p->next = head;

        // Do we get to change head to p?
        while (!atomic_compare_exchange_weak(&head, &p->next, p)) {
            // Nope, someone got there first, try again with the new p->next,
            // and remember: p->next is automatically changed to the new value of head.
        }
        // Yup, great! Everything's done then.
    }

    // Used by consumers to claim items to process.
    int pop_work() {
        auto p = atomic_load(&head); // The item we want to process.
        int work = (p ? p->work : -1);

        // Do we get to change head to p->next?
        while (p && !atomic_compare_exchange_weak(&head, &p, p->next)) {
            // Nope, someone got there first, try again with the new p,
            // and remember: p is automatically changed to the new value of head.
            work = (p ? p->work : -1); // Make sure to update work as well!
        }
        // Yup, great! Everything's done then, return the new task.
        return work; // Returns -1 if list is empty.
    }
};

修改:在演讲中解释了将shared_ptratomic_*功能结合使用的原因。简而言之:从链表中弹出一个项目可能会从遍历列表的人员下面删除它,或者可能会在同一个内存地址(The ABA Problem)上分配不同的节点。使用shared_ptr将确保任何旧读者都拥有对原始项目的有效引用。

正如赫伯所解释的那样,这使得pop功能变得微不足道。

答案 2 :(得分:0)

我不知道如何在C ++ 11(或更高版本)中执行此操作;但是,这里有一个解决方案,用于如何使用C ++ 98和`boost(v1.50):

这显然不是一个非常有用的例子,它仅用于示范目的:

#include <boost/scoped_ptr.hpp>
#include <boost/function.hpp>
#include <boost/asio/io_service.hpp>
#include <boost/thread.hpp>

class WorkHandler
{
public:
    WorkHandler();
    ~WorkHandler();

    typedef boost::function<void(void)> Work; // the type of work we can handle
    void AddWork(Work w) { pThreadProcessing->post(w); }

private:
    void ProcessWork();

    boost::scoped_ptr<boost::asio::io_service> pThreadProcessing;
    boost::thread thread;
    bool runThread; // Make sure this is atomic
};

WorkHandler::WorkHandler()
: pThreadProcessing(new boost::asio::io_service), // create our io service
thread(&WorkHandler::ProcessWork, this), // create our thread
runThread(true) // run the thread
{
}

WorkHandler::~WorkHandler()
{
    runThread = false; // stop running the thread
    thread.join(); // wait for the thread to finish
}

void WorkHandler::ProcessWork()
{
    while (runThread) // while the thread is running
    {
        pThreadProcessing->run(); // process work
        pThreadProcessing->reset(); // prepare for more work
    }
}

int CalculateSomething(int a, int b)
{
    return a + b;
}

int main()
{
    WorkHandler wh; // create a work handler
    // give it some work to do
    wh.AddWork(boost::bind(&CalculateSomething, 4, 5));
    wh.AddWork(boost::bind(&CalculateSomething, 10, 100));
    wh.AddWork(boost::bind(&CalculateSomething, 35, -1));
    Sleep(2000); // ONLY for demonstration! This just allows the thread a chance to work before we destroy it.
    return 0;
}

boost::asio::io_service是线程安全的,因此您可以将工作发布到它而无需使用互斥锁。

注意:虽然我还没有制作bool runThread原子,但为了线程安全,它应该是(我只是在我的c ++中没有atomic)< /强>