Thread: Sound lags in multi-thread version

Hybrid View

Previous Post Previous Post   Next Post Next Post
  1. #1
    Registered User Codeplug's Avatar
    Join Date
    Mar 2003
    Posts
    4,981
    Here's a simple shared Q that I often use:
    Code:
    #include <deque>
    #include <stdexcept>
    
    template <typename T>
    class deque_MT
    {
        mutable CRITICAL_SECTION m_cs;
        mutable HANDLE m_consumerEvent;
    
        // RAII for CS
        struct Lock
        {
            CRITICAL_SECTION &m_cs;
            Lock(CRITICAL_SECTION &cs) : m_cs(cs) {::EnterCriticalSection(&m_cs);}
            ~Lock() {::LeaveCriticalSection(&m_cs);}
        };//Lock
    
        std::deque<T> m_queue;
    
    public:
        deque_MT() throw (runtime_error)
        {
            m_consumerEvent = ::CreateEventW(0, TRUE, FALSE, 0);
            if (!m_consumerEvent)
                throw std::runtime_error("deque_MT::deque_MT() CreateEvent failed");
            
            if (!::InitializeCriticalSectionAndSpinCount(&m_cs, 4000))
            {
                ::CloseHandle(m_consumerEvent);
                throw std::runtime_error("deque_MT::deque_MT() Init-CS failed");
            }//if
        }//constructor
    
        ~deque_MT()
        {
            ::CloseHandle(m_consumerEvent);
            ::DeleteCriticalSection(&m_cs);
        }//destructor
    
        HANDLE consumer_event() const {return m_consumerEvent;}
    
        size_t size() const {Lock l(m_cs); return m_queue.size();}
        bool empty() const {return size() == 0;}
    
        void clear() 
        {
            Lock l(m_cs); 
            m_queue.clear();
            ::ResetEvent(m_consumerEvent);
        }//clear
    
        // Add an object to the queue and signal the consumer event
        void enqueue(const T& val)
        {
            Lock l(m_cs); 
            m_queue.push_front(val);
            ::SetEvent(m_consumerEvent);
        }//enqueue
    
        // Remove next object from the queue, returns false if Q is empty
        // NOTE: With multiple consumers, event can be signaled with an empty Q
        bool dequeue(T &val) throw(runtime_error)
        {
            Lock l(m_cs);
            size_t sz = m_queue.size();
            if (sz == 0)
                return false;
    
            if (sz == 1) // this will drain the Q
                ::ResetEvent(m_consumerEvent);
    
            val = m_queue.back();
            m_queue.pop_back();
            return true;
        }//dequeue
    };//class deque_MT
    Things to note:

    STL container completely protected by CS.

    Manual reset event - stays signaled until Q is drained.

    Scope of CS limited to only what needs protection. I noticed that Play(m_PlayingID) is called within the scope of m_CS - which would unnecessarily block threads calling DXSoundEmitter::playSound(), diminishing throughput. If multiple "things" need synchronization, consider using a synchronization object for each "thing".

    So here is how I would re-factor the posted code:
    Code:
    void DXSoundEmitter::Loop()
    {
        const size_t NumWaitObjs = 2;
        const HANDLE WaitObjs[NumWaitObjs] =
        {
            m_ExitEvent, // <-- new member variable, auto reset event
            m_PlayList.consumer_event(),
        };//WaitObjs
    
        enum
        {
            // must keep these in sync with WaitObjs array
            EXIT_SIGNALED     = WAIT_OBJECT_0 + 0,
            PLAYLIST_SIGNALED = WAIT_OBJECT_0 + 1,
        };
    
        DWORD status = WaitForMultipleObjects(NumWaitObjs, WaitObjs, 
                                              FALSE, INFINITE);
        if (status == PLAYLIST_SIGNALED)
        {
            // if only a single consumer (ie this is the only thread that can cause
            // the consumer event to reset), then a signaled event guarantees there's
            // something in the Q - but safety first
            unsigned int nextID;
            if (m_PlayList.dequeue(nextID))
            {
                m_PlayingID = nextID;
                Play(m_PlayingID);
            }//if
        }//if
        else if (status == EXIT_SIGNALED)
        {
            Stop();
            return;
        }//else if
        else
        {
            // TODO - log unexpected return from WFMO
        }//else
    }//Loop
    
    void DXSoundEmitter::playSound(unsigned int ID)
    {
        m_PlayList.enqueue(ID);
    }//playSound
    
    void DXSoundEmitter::flushThread()
    {
        ::SetEvent(m_ExitEvent);
    }//flushThread
    I used another event for exit notification. Not much different from what you had - but since deque_MT<> took ownership of the the protecting CS, another synchronization object was needed. If m_SoundState ever has more states than just run/stop, what you can do is rename m_ExitEvent to m_SoundStateEvent - then signal the event whenever a state change is made externally (you'll need to assume atomicity of reads/writes or use interlocked reads/writes).

    When using WFMO (and not waiting on all objects to be signaled), keep in mind that the first event in the array has "priority" over the second event. So in the above example, if the exit event were second, the thread may not exit as quickly as you would like (or at all) under a high m_PlayList load.

    As an exercise, an alternative method for signaling thread exit could be to use a sentinel ID of 0:
    Code:
    void DXSoundEmitter::playSound(unsigned int ID)
    {
        if (ID != 0)
            m_PlayList.enqueue(ID);
    }//playSound
    
    void DXSoundEmitter::flushThread()
    {
        m_PlayList.clear();
        m_PlayList.enqueue(0);
    }//flushThread
    Couple things to note here: The call to clear() will reset the consumer event. This means that the "status == PLAYLIST_SIGNALED" code could be executing even though the Q is actually empty. We check for a false return from dequeue() so we're ok there.
    You may also be thinking: "Hey, we can now spin on WFSO instead of WFMO. Surely, checking a single event is faster than checking an array of events!" Premature optimization at its finest! The metric to monitor in this case is throughput. Any small speedup in the looping construct would be completely washed out by the overhead incurred by the work-horse: Play().

    So that "exercise" was really a lesson in premature optimization - not that anyone *needed* a lesson of course :) I simply found myself needing the lesson as I thought "how can I make this exit notification as fast and efficient as possible?". As always, follow K.I.S.S., profile, then optimize.

    gg
    Last edited by Codeplug; 08-27-2008 at 08:30 AM.

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. Problem building Quake source
    By Silvercord in forum Game Programming
    Replies: 16
    Last Post: 07-11-2010, 09:13 AM
  2. C++ Threading?
    By draggy in forum C++ Programming
    Replies: 5
    Last Post: 08-16-2005, 12:16 PM
  3. [code] Win32 Thread Object
    By Codeplug in forum Windows Programming
    Replies: 0
    Last Post: 06-03-2005, 03:55 PM
  4. Low latency sound effects
    By VirtualAce in forum Game Programming
    Replies: 0
    Last Post: 12-21-2004, 01:58 AM
  5. Win32 Thread Object Model Revisted
    By Codeplug in forum Windows Programming
    Replies: 5
    Last Post: 12-15-2004, 08:50 AM