【问题标题】:Event / Task Queue Multithreading C++事件/任务队列多线程 C++
【发布时间】:2010-10-29 18:20:10
【问题描述】:

我想创建一个可以从多个线程调用其方法的类。但不是在调用它的线程中执行该方法,而是应该在它自己的线程中执行它们。不需要返回结果,也不应该阻塞调用线程。

我在下面包含的第一次尝试实现。公共方法将函数指针和数据插入到作业队列中,然后工作线程将拾取该作业队列。然而,它并不是特别好的代码,而且添加新方法很麻烦。

理想情况下,我想将其用作基类,我可以轻松地添加方法(具有可变数量的参数),同时减少麻烦和代码重复。

有什么更好的方法来做到这一点?是否有任何现有的代码可以做类似的事情?谢谢

#include <queue>

using namespace std;

class GThreadObject
{
    class event
    {
        public:
        void (GThreadObject::*funcPtr)(void *);
        void * data;
    };

public:
    void functionOne(char * argOne, int argTwo);

private:
    void workerThread();
    queue<GThreadObject::event*> jobQueue;
    void functionOneProxy(void * buffer);
    void functionOneInternal(char * argOne, int argTwo);

};



#include <iostream>
#include "GThreadObject.h"

using namespace std;

/* On a continuous loop, reading tasks from queue
 * When a new event is received it executes the attached function pointer
 * It should block on a condition, but Thread code removed to decrease clutter
 */
void GThreadObject::workerThread()
{
    //New Event added, process it
    GThreadObject::event * receivedEvent = jobQueue.front();

    //Execute the function pointer with the attached data
    (*this.*receivedEvent->funcPtr)(receivedEvent->data);
}

/*
 * This is the public interface, Can be called from child threads
 * Instead of executing the event directly it adds it to a job queue
 * Then the workerThread picks it up and executes all tasks on the same thread
 */
void GThreadObject::functionOne(char * argOne, int argTwo)
{

    //Malloc an object the size of the function arguments
    int argumentSize = sizeof(char*)+sizeof(int);
    void * myData = malloc(argumentSize);
    //Copy the data passed to this function into the buffer
    memcpy(myData, &argOne, argumentSize);

    //Create the event and push it on to the queue
    GThreadObject::event * myEvent = new event;
    myEvent->data = myData;
    myEvent->funcPtr = &GThreadObject::functionOneProxy;
    jobQueue.push(myEvent);

    //This would be send a thread condition signal, replaced with a simple call here
    this->workerThread();
}

/*
 * This handles the actual event
 */
void GThreadObject::functionOneInternal(char * argOne, int argTwo)
{
    cout << "We've made it to functionTwo char*:" << argOne << " int:" << argTwo << endl;

    //Now do the work
}

/*
 * This is the function I would like to remove if possible
 * Split the void * buffer into arguments for the internal Function
 */
void GThreadObject::functionOneProxy(void * buffer)
{
    char * cBuff = (char*)buffer;
    functionOneInternal((char*)*((unsigned int*)cBuff), (int)*(cBuff+sizeof(char*)));
};

int main()
{
    GThreadObject myObj;

    myObj.functionOne("My Message", 23);

    return 0;
}

【问题讨论】:

    标签: c++ multithreading queue pthreads


    【解决方案1】:

    Futures 库正在进入 Boost 和 C++ 标准库。 ACE 中也有类似的东西,但我不愿意向任何人推荐它(正如@lothar 已经指出的那样,它是 Active Object。)

    【讨论】:

    • 我一直在寻找 boost::futures,但由于它不是已发布的 boost 版本的一部分,我不得不退回到我信任的 ACE :-)
    • future 库将成为 Boost 1.41 的一部分。它也可以作为我的 C++0x 线程库实现的一部分在stdthread.co.uk
    • 谢谢,安东尼。很高兴收到你的来信:)
    【解决方案2】:

    下面是一个不需要“functionProxy”方法的实现。尽管添加新方法更容易,但仍然很混乱。

    Boost::Bind 和 "Futures" 似乎确实会整理很多东西。我想我会看看 boost 代码,看看它是如何工作的。谢谢大家的建议。

    GThreadObject.h

    #include <queue>
    
    using namespace std;
    
    class GThreadObject
    {
    
        template <int size>
        class VariableSizeContainter
        {
            char data[size];
        };
    
        class event
        {
            public:
            void (GThreadObject::*funcPtr)(void *);
            int dataSize;
            char * data;
        };
    
    public:
        void functionOne(char * argOne, int argTwo);
        void functionTwo(int argTwo, int arg2);
    
    
    private:
        void newEvent(void (GThreadObject::*)(void*), unsigned int argStart, int argSize);
        void workerThread();
        queue<GThreadObject::event*> jobQueue;
        void functionTwoInternal(int argTwo, int arg2);
        void functionOneInternal(char * argOne, int argTwo);
    
    };
    

    GThreadObject.cpp

    #include <iostream>
    #include "GThreadObject.h"
    
    using namespace std;
    
    /* On a continuous loop, reading tasks from queue
     * When a new event is received it executes the attached function pointer
     * Thread code removed to decrease clutter
     */
    void GThreadObject::workerThread()
    {
        //New Event added, process it
        GThreadObject::event * receivedEvent = jobQueue.front();
    
        /* Create an object the size of the stack the function is expecting, then cast the function to accept this object as an argument.
         * This is the bit i would like to remove
         * Only supports 8 byte argument size e.g 2 int's OR pointer + int OR myObject8bytesSize
         * Subsequent data sizes would need to be added with an else if
         * */
        if (receivedEvent->dataSize == 8)
        {
            const int size = 8;
    
            void (GThreadObject::*newFuncPtr)(VariableSizeContainter<size>);
            newFuncPtr = (void (GThreadObject::*)(VariableSizeContainter<size>))receivedEvent->funcPtr;
    
            //Execute the function
            (*this.*newFuncPtr)(*((VariableSizeContainter<size>*)receivedEvent->data));
        }
    
        //Clean up
        free(receivedEvent->data);
        delete receivedEvent;
    
    }
    
    void GThreadObject::newEvent(void (GThreadObject::*funcPtr)(void*), unsigned int argStart, int argSize)
    {
    
        //Malloc an object the size of the function arguments
        void * myData = malloc(argSize);
        //Copy the data passed to this function into the buffer
        memcpy(myData, (char*)argStart, argSize);
    
        //Create the event and push it on to the queue
        GThreadObject::event * myEvent = new event;
        myEvent->data = (char*)myData;
        myEvent->dataSize = argSize;
        myEvent->funcPtr = funcPtr;
        jobQueue.push(myEvent);
    
        //This would be send a thread condition signal, replaced with a simple call here
        this->workerThread();
    
    }
    
    /*
     * This is the public interface, Can be called from child threads
     * Instead of executing the event directly it adds it to a job queue
     * Then the workerThread picks it up and executes all tasks on the same thread
     */
    void GThreadObject::functionOne(char * argOne, int argTwo)
    {
        newEvent((void (GThreadObject::*)(void*))&GThreadObject::functionOneInternal, (unsigned int)&argOne, sizeof(char*)+sizeof(int));
    }
    
    /*
     * This handles the actual event
     */
    void GThreadObject::functionOneInternal(char * argOne, int argTwo)
    {
        cout << "We've made it to functionOne Internal char*:" << argOne << " int:" << argTwo << endl;
    
        //Now do the work
    }
    
    void GThreadObject::functionTwo(int argOne, int argTwo)
    {
        newEvent((void (GThreadObject::*)(void*))&GThreadObject::functionTwoInternal, (unsigned int)&argOne, sizeof(int)+sizeof(int));
    }
    
    /*
     * This handles the actual event
     */
    void GThreadObject::functionTwoInternal(int argOne, int argTwo)
    {
        cout << "We've made it to functionTwo Internal arg1:" << argOne << " int:" << argTwo << endl;
    }
    

    main.cpp

    #include <iostream>
    #include "GThreadObject.h"
    
    int main()
    {
    
        GThreadObject myObj;
    
        myObj.functionOne("My Message", 23);
        myObj.functionTwo(456, 23);
    
    
        return 0;
    }
    

    编辑:为了完整起见,我使用 Boost::bind 进行了实现。主要区别:

    queue<boost::function<void ()> > jobQueue;
    
    void GThreadObjectBoost::functionOne(char * argOne, int argTwo)
    {
        jobQueue.push(boost::bind(&GThreadObjectBoost::functionOneInternal, this, argOne, argTwo));
    
        workerThread();
    }
    
    void GThreadObjectBoost::workerThread()
    {
        boost::function<void ()> func = jobQueue.front();
        func();
    }
    

    对 functionOne() 的 10,000,000 次迭代使用 boost 实现大约需要 19 秒。然而,非升压实施仅花费了约 6.5 秒。所以慢了大约 3 倍。我猜想找到一个好的非锁定队列将是这里最大的性能瓶颈。但是还是有很大区别的。

    【讨论】:

      【解决方案3】:

      POCO 库在线程部分有一些类似的东西,称为 ActiveMethod(以及一些相关功能,例如 ActiveResult)。源代码随手可得,易于理解。

      【讨论】:

        【解决方案4】:

        您可能对Active Object 中的ACE Patterns 之一感兴趣,ACE framework

        正如 Nikolai 指出的那样,futures 是未来某个时间标准 C++ 的 planned(双关语)。

        【讨论】:

          【解决方案5】:

          为了可扩展性和可维护性(以及其他功能),您可以为线程要执行的“作业”定义一个抽象类(或接口)。然后线程池的用户将实现此接口并将对象引用到线程池。这与 Symbian Active Object 的设计非常相似:每个 AO 都是 CActive 的子类,并且必须实现 Run() 和 Cancel() 等方法。

          为简单起见,您的界面(抽象类)可能如下所示:

          class IJob
          {
              virtual Run()=0;
          };
          

          那么线程池,或者单线程接受请求会是这样的:

          class CThread
          {
             <...>
          public:
             void AddJob(IJob* iTask);
             <...>
          };
          

          当然,您将拥有多个任务,这些任务可以具有各种额外的 setter/getter/attribute 以及您在任何行业中需要的任何东西。但是,唯一必须的是实现方法 Run(),它会执行冗长的计算:

          class CDumbLoop : public IJob
          {
          public:
              CDumbJob(int iCount) : m_Count(iCount) {};
              ~CDumbJob() {};
              void Run()
              {
                  // Do anything you want here
              }
          private:
              int m_Count;
          };
          

          【讨论】:

          • 这是我们在我的公司用于在下一代系统上开发游戏的确切方法(为了获得额外的性能奖励,请考虑通过无锁方法将项目添加到工作队列)
          • 有什么实现无锁队列的技巧吗?
          • 网上有很多很好的无锁队列示例(它是最容易实现的无锁结构之一)。这是我在进入无锁潮流时咬牙切齿的:boyet.com/Articles/LockfreeQueue.html
          • 哦,请注意我链接到的文章存在 ABA 问题,页面顶部有一个详细说明此问题的链接,并且网上有更好的解决方案,但这是我见过的最容易学习的教程之一。
          【解决方案6】:

          您可以使用 Boost 的 Thread 库来解决这个问题。像这样的东西(半伪):

          
          class GThreadObject
          {
                  ...
          
                  public:
                          GThreadObject()
                          : _done(false)
                          , _newJob(false)
                          , _thread(boost::bind(&GThreadObject::workerThread, this))
                          {
                          }
          
                          ~GThreadObject()
                          {
                                  _done = true;
          
                                  _thread.join();
                          }
          
                          void functionOne(char *argOne, int argTwo)
                          {
                                  ...
          
                                  _jobQueue.push(myEvent);
          
                                  {
                                          boost::lock_guard l(_mutex);
          
                                          _newJob = true;
                                  }
          
                                  _cond.notify_one();
                          }
          
                  private:
                          void workerThread()
                          {
                                  while (!_done) {
                                          boost::unique_lock l(_mutex);
          
                                          while (!_newJob) {
                                                  cond.wait(l);
                                          }
          
                                          Event *receivedEvent = _jobQueue.front();
          
                                          ...
                                  }
                          }
          
                  private:
                          volatile bool             _done;
                          volatile bool             _newJob;
                          boost::thread             _thread;
                          boost::mutex              _mutex;
                          boost::condition_variable _cond;
                          std::queue<Event*>        _jobQueue;
          };
          

          另外,请注意RAII 如何使我们能够更小、更好地管理此代码。

          【讨论】:

          • std::queue::push 线程安全吗?似乎 functionOne lock_guard 应该在 _jobQueue.push 调用之前进行。
          【解决方案7】:

          这是我为类似目的而编写的一个类(我将它用于事件处理,但您当然可以将它重命名为 ActionQueue ——并重命名它的方法)。

          你可以这样使用它:

          你要调用的函数:void foo (const int x, const int y) { /*...*/ }

          还有:EventQueue q;

          q.AddEvent (boost::bind (foo, 10, 20));

          在工作线程中

          q.PlayOutEvents();

          注意:添加代码以在条件下阻塞以避免耗尽 CPU 周期应该是相当容易的。

          代码(Visual Studio 2003 with boost 1.34.1):

          #pragma once
          
          #include <boost/thread/recursive_mutex.hpp>
          #include <boost/function.hpp>
          #include <boost/signals.hpp>
          #include <boost/bind.hpp>
          #include <boost/foreach.hpp>
          #include <string>
          using std::string;
          
          
          // Records & plays out actions (closures) in a safe-thread manner.
          
          class EventQueue
          {
              typedef boost::function <void ()> Event;
          
          public:
          
              const bool PlayOutEvents ()
              {
                  // The copy is there to ensure there are no deadlocks.
                  const std::vector<Event> eventsCopy = PopEvents ();
          
                  BOOST_FOREACH (const Event& e, eventsCopy)
                  {
                      e ();
                      Sleep (0);
                  }
          
                  return eventsCopy.size () > 0;
              }
          
              void AddEvent (const Event& event)
              {
                  Mutex::scoped_lock lock (myMutex);
          
                  myEvents.push_back (event);
              }
          
          protected:
          
              const std::vector<Event> PopEvents ()
              {
                  Mutex::scoped_lock lock (myMutex);
          
                  const std::vector<Event> eventsCopy = myEvents;
                  myEvents.clear ();
          
                  return eventsCopy;
              }
          
          private:
          
              typedef boost::recursive_mutex Mutex;
              Mutex myMutex;
          
              std::vector <Event> myEvents;
          
          };
          

          我希望这会有所帮助。 :)

          马丁·比尔斯基

          【讨论】:

          • 我强烈建议使用互斥锁(或任何其他形式的“同步原语”),因为它们根本无法与多个处理器一起扩展(大约 4-8 之后,它们实际上会降低性能)。研究无锁编码以获得真正可扩展的实现。此外,如果您需要使用同步原语,请使用临界区,因为它们比互斥锁更快(互斥锁是进程安全的,临界区是线程安全的,即在进程之间同步时使用互斥锁,在同一线程中同步线程时使用 CS过程)
          【解决方案8】:

          您应该看看 Boost ASIO 库。它旨在异步调度事件。它可以与 Boost Thread 库配对以构建您描述的系统。

          您需要实例化一个boost::asio::io_service 对象并安排一系列异步事件(boost::asio::io_service::postboost::asio::io_service::dispatch)。接下来,您从 n 个线程调用 run 成员函数。 io_service 对象是线程安全的,并保证您的异步处理程序只会在您调用 io_service::run 的线程中分派。

          boost::asio::strand 对象对于简单的线程同步也很有用。

          对于它的价值,我认为 ASIO 库是解决这个问题的一个非常优雅的解决方案。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2014-05-06
            • 2017-05-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多