diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
commit | c04dcc2e7d834218ef2d4194331e383402495ae1 (patch) | |
tree | 7333e38d10d75386e60f336b80c2443c1166031d /xbmc/threads | |
parent | Initial commit. (diff) | |
download | kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip |
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xbmc/threads')
-rw-r--r-- | xbmc/threads/CMakeLists.txt | 17 | ||||
-rw-r--r-- | xbmc/threads/Condition.h | 108 | ||||
-rw-r--r-- | xbmc/threads/CriticalSection.h | 27 | ||||
-rw-r--r-- | xbmc/threads/Event.cpp | 95 | ||||
-rw-r--r-- | xbmc/threads/Event.h | 239 | ||||
-rw-r--r-- | xbmc/threads/IRunnable.h | 17 | ||||
-rw-r--r-- | xbmc/threads/IThreadImpl.h | 43 | ||||
-rw-r--r-- | xbmc/threads/Lockables.h | 107 | ||||
-rw-r--r-- | xbmc/threads/SharedSection.h | 54 | ||||
-rw-r--r-- | xbmc/threads/SingleLock.h | 32 | ||||
-rw-r--r-- | xbmc/threads/SystemClock.h | 96 | ||||
-rw-r--r-- | xbmc/threads/Thread.cpp | 282 | ||||
-rw-r--r-- | xbmc/threads/Thread.h | 125 | ||||
-rw-r--r-- | xbmc/threads/Timer.cpp | 109 | ||||
-rw-r--r-- | xbmc/threads/Timer.h | 51 | ||||
-rw-r--r-- | xbmc/threads/test/CMakeLists.txt | 7 | ||||
-rw-r--r-- | xbmc/threads/test/TestEndTime.cpp | 77 | ||||
-rw-r--r-- | xbmc/threads/test/TestEvent.cpp | 628 | ||||
-rw-r--r-- | xbmc/threads/test/TestHelpers.h | 75 | ||||
-rw-r--r-- | xbmc/threads/test/TestSharedSection.cpp | 215 |
20 files changed, 2404 insertions, 0 deletions
diff --git a/xbmc/threads/CMakeLists.txt b/xbmc/threads/CMakeLists.txt new file mode 100644 index 0000000..02e2016 --- /dev/null +++ b/xbmc/threads/CMakeLists.txt @@ -0,0 +1,17 @@ +set(SOURCES Event.cpp + Thread.cpp + Timer.cpp) + +set(HEADERS Condition.h + CriticalSection.h + Event.h + Lockables.h + SharedSection.h + SingleLock.h + SystemClock.h + Thread.h + Timer.h + IThreadImpl.h + IRunnable.h) + +core_add_library(threads) diff --git a/xbmc/threads/Condition.h b/xbmc/threads/Condition.h new file mode 100644 index 0000000..3738352 --- /dev/null +++ b/xbmc/threads/Condition.h @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" + +#include <chrono> +#include <condition_variable> +#include <functional> +#include <mutex> +#include <utility> + +namespace XbmcThreads +{ + + /** + * This is a thin wrapper around std::condition_variable_any. It is subject + * to "spurious returns" + */ + class ConditionVariable + { + private: + std::condition_variable_any cond; + ConditionVariable(const ConditionVariable&) = delete; + ConditionVariable& operator=(const ConditionVariable&) = delete; + + public: + ConditionVariable() = default; + + inline void wait(CCriticalSection& lock, std::function<bool()> predicate) + { + int count = lock.count; + lock.count = 0; + cond.wait(lock.get_underlying(), std::move(predicate)); + lock.count = count; + } + + inline void wait(CCriticalSection& lock) + { + int count = lock.count; + lock.count = 0; + cond.wait(lock.get_underlying()); + lock.count = count; + } + + template<typename Rep, typename Period> + inline bool wait(CCriticalSection& lock, + std::chrono::duration<Rep, Period> duration, + std::function<bool()> predicate) + { + int count = lock.count; + lock.count = 0; + bool ret = cond.wait_for(lock.get_underlying(), duration, predicate); + lock.count = count; + return ret; + } + + template<typename Rep, typename Period> + inline bool wait(CCriticalSection& lock, std::chrono::duration<Rep, Period> duration) + { + int count = lock.count; + lock.count = 0; + std::cv_status res = cond.wait_for(lock.get_underlying(), duration); + lock.count = count; + return res == std::cv_status::no_timeout; + } + + inline void wait(std::unique_lock<CCriticalSection>& lock, std::function<bool()> predicate) + { + cond.wait(*lock.mutex(), std::move(predicate)); + } + + inline void wait(std::unique_lock<CCriticalSection>& lock) { wait(*lock.mutex()); } + + template<typename Rep, typename Period> + inline bool wait(std::unique_lock<CCriticalSection>& lock, + std::chrono::duration<Rep, Period> duration, + std::function<bool()> predicate) + { + return wait(*lock.mutex(), duration, predicate); + } + + template<typename Rep, typename Period> + inline bool wait(std::unique_lock<CCriticalSection>& lock, + std::chrono::duration<Rep, Period> duration) + { + return wait(*lock.mutex(), duration); + } + + inline void notifyAll() + { + cond.notify_all(); + } + + inline void notify() + { + cond.notify_one(); + } + }; + +} + diff --git a/xbmc/threads/CriticalSection.h b/xbmc/threads/CriticalSection.h new file mode 100644 index 0000000..5aaf8aa --- /dev/null +++ b/xbmc/threads/CriticalSection.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/Lockables.h" + +#if defined(TARGET_POSIX) +#include "platform/posix/threads/RecursiveMutex.h" + +class CCriticalSection : public XbmcThreads::CountingLockable<XbmcThreads::CRecursiveMutex> +{ +}; + +#elif defined(TARGET_WINDOWS) +#include <mutex> + +class CCriticalSection : public XbmcThreads::CountingLockable<std::recursive_mutex> +{ +}; + +#endif diff --git a/xbmc/threads/Event.cpp b/xbmc/threads/Event.cpp new file mode 100644 index 0000000..1c67f4e --- /dev/null +++ b/xbmc/threads/Event.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2002 Frodo + * Portions Copyright (c) by the authors of ffmpeg and xvid + * Copyright (C) 2002-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "Event.h" + +#include <algorithm> +#include <limits> +#include <mutex> + +using namespace std::chrono_literals; + +void CEvent::addGroup(XbmcThreads::CEventGroup* group) +{ + std::unique_lock<CCriticalSection> lock(groupListMutex); + if (!groups) + groups.reset(new std::vector<XbmcThreads::CEventGroup*>); + + groups->push_back(group); +} + +void CEvent::removeGroup(XbmcThreads::CEventGroup* group) +{ + std::unique_lock<CCriticalSection> lock(groupListMutex); + if (groups) + { + groups->erase(std::remove(groups->begin(), groups->end(), group), groups->end()); + if (groups->empty()) + { + groups.reset(); + } + } +} + +// locking is ALWAYS done in this order: +// CEvent::groupListMutex -> CEventGroup::mutex -> CEvent::mutex +void CEvent::Set() +{ + // Originally I had this without locking. Thanks to FernetMenta who + // pointed out that this creates a race condition between setting + // checking the signal and calling wait() on the Wait call in the + // CEvent class. This now perfectly matches the boost example here: + // http://www.boost.org/doc/libs/1_41_0/doc/html/thread/synchronization.html#thread.synchronization.condvar_ref + { + std::unique_lock<CCriticalSection> slock(mutex); + signaled = true; + } + + actualCv.notifyAll(); + + std::unique_lock<CCriticalSection> l(groupListMutex); + if (groups) + { + for (auto* group : *groups) + group->Set(this); + } +} + +namespace XbmcThreads +{ + /** + * This will block until any one of the CEvents in the group are + * signaled at which point a pointer to that CEvents will be + * returned. + */ + CEvent* CEventGroup::wait() + { + return wait(std::chrono::milliseconds::max()); + } + + CEventGroup::CEventGroup(std::initializer_list<CEvent*> eventsList) + : events{eventsList} + { + // we preping for a wait, so we need to set the group value on + // all of the CEvents. + for (auto* event : events) + { + event->addGroup(this); + } + } + + CEventGroup::~CEventGroup() + { + for (auto* event : events) + { + event->removeGroup(this); + } + } +} diff --git a/xbmc/threads/Event.h b/xbmc/threads/Event.h new file mode 100644 index 0000000..72ce754 --- /dev/null +++ b/xbmc/threads/Event.h @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/Condition.h" + +#include <initializer_list> +#include <memory> +#include <mutex> +#include <vector> + +// forward declare the CEventGroup +namespace XbmcThreads +{ +class CEventGroup; +} + +/** + * @brief This is an Event class built from a ConditionVariable. The Event adds the state + * that the condition is gating as well as the mutex/lock. + * + * This Event can be 'interruptible' (even though there is only a single place + * in the code that uses this behavior). + * + * This class manages 'spurious returns' from the condition variable. + * + */ + +class CEvent +{ + bool manualReset; + volatile bool signaled; + unsigned int numWaits = 0; + + CCriticalSection groupListMutex; // lock for the groups list + std::unique_ptr<std::vector<XbmcThreads::CEventGroup*>> groups; + + XbmcThreads::ConditionVariable actualCv; + CCriticalSection mutex; + + friend class XbmcThreads::CEventGroup; + + void addGroup(XbmcThreads::CEventGroup* group); + void removeGroup(XbmcThreads::CEventGroup* group); + + // helper for the two wait methods + inline bool prepReturn() + { + bool ret = signaled; + if (!manualReset && numWaits == 0) + signaled = false; + return ret; + } + + CEvent(const CEvent&) = delete; + CEvent& operator=(const CEvent&) = delete; + +public: + inline CEvent(bool manual = false, bool signaled_ = false) + : manualReset(manual), signaled(signaled_) + { + } + + inline void Reset() + { + std::unique_lock<CCriticalSection> lock(mutex); + signaled = false; + } + void Set(); + + /** + * @brief Returns true if Event has been triggered and not reset, false otherwise. + * + */ + inline bool Signaled() + { + std::unique_lock<CCriticalSection> lock(mutex); + return signaled; + } + + /** + * @brief This will wait up to 'duration' for the Event to be + * triggered. The method will return 'true' if the Event + * was triggered. Otherwise it will return false. + * + */ + template<typename Rep, typename Period> + inline bool Wait(std::chrono::duration<Rep, Period> duration) + { + std::unique_lock<CCriticalSection> lock(mutex); + numWaits++; + actualCv.wait(mutex, duration, std::bind(&CEvent::Signaled, this)); + numWaits--; + return prepReturn(); + } + + /** + * @brief This will wait for the Event to be triggered. The method will return + * 'true' if the Event was triggered. If it was either interrupted + * it will return false. Otherwise it will return false. + * + */ + inline bool Wait() + { + std::unique_lock<CCriticalSection> lock(mutex); + numWaits++; + actualCv.wait(mutex, std::bind(&CEvent::Signaled, this)); + numWaits--; + return prepReturn(); + } + + /** + * @brief This is mostly for testing. It allows a thread to make sure there are + * the right amount of other threads waiting. + * + */ + inline int getNumWaits() + { + std::unique_lock<CCriticalSection> lock(mutex); + return numWaits; + } +}; + +namespace XbmcThreads +{ +/** + * @brief CEventGroup is a means of grouping CEvents to wait on them together. + * It is equivalent to WaitOnMultipleObject that returns when "any" Event + * in the group signaled. + * + */ +class CEventGroup +{ + std::vector<CEvent*> events; + CEvent* signaled{}; + XbmcThreads::ConditionVariable actualCv; + CCriticalSection mutex; + + unsigned int numWaits{0}; + + // This is ONLY called from CEvent::Set. + inline void Set(CEvent* child) + { + std::unique_lock<CCriticalSection> l(mutex); + signaled = child; + actualCv.notifyAll(); + } + + friend class ::CEvent; + + CEventGroup(const CEventGroup&) = delete; + CEventGroup& operator=(const CEventGroup&) = delete; + +public: + /** + * @brief Create a CEventGroup from a number of CEvents. + * + */ + CEventGroup(std::initializer_list<CEvent*> events); + + ~CEventGroup(); + + /** + * @brief This will block until any one of the CEvents in the group are + * signaled at which point a pointer to that CEvents will be + * returned. + * + */ + CEvent* wait(); + + /** + * @brief locking is ALWAYS done in this order: + * CEvent::groupListMutex -> CEventGroup::mutex -> CEvent::mutex + * + * Notice that this method doesn't grab the CEvent::groupListMutex at all. This + * is fine. It just grabs the CEventGroup::mutex and THEN the individual + * + */ + template<typename Rep, typename Period> + CEvent* wait(std::chrono::duration<Rep, Period> duration) + { + std::unique_lock<CCriticalSection> lock(mutex); // grab CEventGroup::mutex + numWaits++; + + // ================================================== + // This block checks to see if any child events are + // signaled and sets 'signaled' to the first one it + // finds. + // ================================================== + signaled = nullptr; + for (auto* cur : events) + { + std::unique_lock<CCriticalSection> lock2(cur->mutex); + if (cur->signaled) + signaled = cur; + } + // ================================================== + + if (!signaled) + { + // both of these release the CEventGroup::mutex + if (duration == std::chrono::duration<Rep, Period>::max()) + actualCv.wait(mutex, [this]() { return signaled != nullptr; }); + else + actualCv.wait(mutex, duration, [this]() { return signaled != nullptr; }); + } // at this point the CEventGroup::mutex is reacquired + numWaits--; + + // signaled should have been set by a call to CEventGroup::Set + CEvent* ret = signaled; + if (numWaits == 0) + { + if (signaled) + // This acquires and releases the CEvent::mutex. This is fine since the + // CEventGroup::mutex is already being held + signaled->Wait(std::chrono::duration<Rep, Period>::zero()); // reset the event if needed + signaled = nullptr; // clear the signaled if all the waiters are gone + } + return ret; + } + + /** + * @brief This is mostly for testing. It allows a thread to make sure there are + * the right amount of other threads waiting. + * + */ + inline int getNumWaits() + { + std::unique_lock<CCriticalSection> lock(mutex); + return numWaits; + } +}; +} // namespace XbmcThreads diff --git a/xbmc/threads/IRunnable.h b/xbmc/threads/IRunnable.h new file mode 100644 index 0000000..32c5b8f --- /dev/null +++ b/xbmc/threads/IRunnable.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +class IRunnable +{ +public: + virtual void Run()=0; + virtual void Cancel() {} + virtual ~IRunnable() = default; +}; diff --git a/xbmc/threads/IThreadImpl.h b/xbmc/threads/IThreadImpl.h new file mode 100644 index 0000000..f631c79 --- /dev/null +++ b/xbmc/threads/IThreadImpl.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-2022 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/Thread.h" + +#include <memory> +#include <string> +#include <thread> + +class IThreadImpl +{ +public: + virtual ~IThreadImpl() = default; + + static std::unique_ptr<IThreadImpl> CreateThreadImpl(std::thread::native_handle_type handle); + + /*! + * \brief Set the thread name and other info (platform dependent) + * + */ + virtual void SetThreadInfo(const std::string& name) = 0; + + /*! + * \brief Set the thread priority via the native threading library + * + */ + virtual bool SetPriority(const ThreadPriority& priority) = 0; + +protected: + IThreadImpl(std::thread::native_handle_type handle) : m_handle(handle) {} + + std::thread::native_handle_type m_handle; + +private: + IThreadImpl() = delete; +}; diff --git a/xbmc/threads/Lockables.h b/xbmc/threads/Lockables.h new file mode 100644 index 0000000..18509b1 --- /dev/null +++ b/xbmc/threads/Lockables.h @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +namespace XbmcThreads +{ + + /** + * This template will take any implementation of the "Lockable" concept + * and allow it to be used as an "Exitable Lockable." + * + * Something that implements the "Lockable concept" simply means that + * it has the three methods: + * + * lock(); + * try_lock(); + * unlock(); + * IsLocked(); + * + * "Exitable" specifically means that, no matter how deep the recursion + * on the mutex/critical section, we can exit from it and then restore + * the state. + * + * This requires us to extend the Lockable so that we can keep track of the + * number of locks that have been recursively acquired so that we can + * undo it, and then restore that (See class CSingleExit). + * + * All xbmc code expects Lockables to be recursive. + */ + template<class L> class CountingLockable + { + friend class ConditionVariable; + + CountingLockable(const CountingLockable&) = delete; + CountingLockable& operator=(const CountingLockable&) = delete; + protected: + L mutex; + unsigned int count = 0; + + public: + inline CountingLockable() = default; + + // STL Lockable concept + inline void lock() { mutex.lock(); count++; } + inline bool try_lock() { return mutex.try_lock() ? count++, true : false; } + inline void unlock() { count--; mutex.unlock(); } + + /*! + * \brief Check if have a lock owned + * \return True if have a lock, otherwise false + */ + inline bool IsLocked() const { return count > 0; } + + /** + * This implements the "exitable" behavior mentioned above. + */ + inline unsigned int exit(unsigned int leave = 0) + { + // it's possible we don't actually own the lock + // so we will try it. + unsigned int ret = 0; + if (try_lock()) + { + if (leave < (count - 1)) + { + ret = count - 1 - leave; // The -1 is because we don't want + // to count the try_lock increment. + // We must NOT compare "count" in this loop since + // as soon as the last unlock is called another thread + // can modify it. + for (unsigned int i = 0; i < ret; i++) + unlock(); + } + unlock(); // undo the try_lock before returning + } + + return ret; + } + + /** + * Restore a previous exit to the provided level. + */ + inline void restore(unsigned int restoreCount) + { + for (unsigned int i = 0; i < restoreCount; i++) + lock(); + } + + /** + * Some implementations (see pthreads) require access to the underlying + * CCriticalSection, which is also implementation specific. This + * provides access to it through the same method on the guard classes + * UniqueLock, and SharedLock. + * + * There really should be no need for the users of the threading library + * to call this method. + */ + inline L& get_underlying() { return mutex; } + }; + +} diff --git a/xbmc/threads/SharedSection.h b/xbmc/threads/SharedSection.h new file mode 100644 index 0000000..dce371d --- /dev/null +++ b/xbmc/threads/SharedSection.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/Condition.h" + +#include <mutex> +#include <shared_mutex> + +/** + * A CSharedSection is a mutex that satisfies the Shared Lockable concept (see Lockables.h). + */ +class CSharedSection +{ + CCriticalSection sec; + XbmcThreads::ConditionVariable actualCv; + + unsigned int sharedCount = 0; + +public: + inline CSharedSection() = default; + + inline void lock() + { + std::unique_lock<CCriticalSection> l(sec); + while (sharedCount) + actualCv.wait(l, [this]() { return sharedCount == 0; }); + sec.lock(); + } + inline bool try_lock() { return (sec.try_lock() ? ((sharedCount == 0) ? true : (sec.unlock(), false)) : false); } + inline void unlock() { sec.unlock(); } + + inline void lock_shared() + { + std::unique_lock<CCriticalSection> l(sec); + sharedCount++; + } + inline bool try_lock_shared() { return (sec.try_lock() ? sharedCount++, sec.unlock(), true : false); } + inline void unlock_shared() + { + std::unique_lock<CCriticalSection> l(sec); + sharedCount--; + if (!sharedCount) + { + actualCv.notifyAll(); + } + } +}; diff --git a/xbmc/threads/SingleLock.h b/xbmc/threads/SingleLock.h new file mode 100644 index 0000000..cacb691 --- /dev/null +++ b/xbmc/threads/SingleLock.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2002 Frodo + * Portions Copyright (c) by the authors of ffmpeg and xvid + * Copyright (C) 2002-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" + +#include <mutex> + +/** + * This implements a "guard" pattern for exiting all locks + * currently being held by the current thread and restoring + * those locks on destruction. + * + * This class can be used on a CCriticalSection that isn't owned + * by this thread in which case it will do nothing. + */ +class CSingleExit +{ + CCriticalSection& sec; + unsigned int count; +public: + inline explicit CSingleExit(CCriticalSection& cs) : sec(cs), count(cs.exit()) { } + inline ~CSingleExit() { sec.restore(count); } +}; diff --git a/xbmc/threads/SystemClock.h b/xbmc/threads/SystemClock.h new file mode 100644 index 0000000..92c4901 --- /dev/null +++ b/xbmc/threads/SystemClock.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "utils/log.h" + +#include <chrono> +#include <limits> +#include <thread> + +namespace XbmcThreads +{ + +template<typename> +struct is_chrono_duration : std::false_type +{ +}; + +template<typename Rep, typename Period> +struct is_chrono_duration<std::chrono::duration<Rep, Period>> : std::true_type +{ +}; + +template<typename T = std::chrono::milliseconds, bool = is_chrono_duration<T>::value> +class EndTime; + +template<typename T> +class EndTime<T, true> +{ +public: + explicit EndTime(const T duration) { Set(duration); } + + EndTime() = default; + EndTime(const EndTime& right) = delete; + ~EndTime() = default; + + static constexpr T Max() { return m_max; } + + void Set(const T duration) + { + m_startTime = std::chrono::steady_clock::now(); + + if (duration > m_max) + { + m_totalWaitTime = m_max; + CLog::Log(LOGWARNING, "duration ({}) greater than max ({}) - duration will be truncated!", + duration.count(), m_max.count()); + } + else + { + m_totalWaitTime = duration; + } + } + + bool IsTimePast() const + { + const auto now = std::chrono::steady_clock::now(); + + return ((now - m_startTime) >= m_totalWaitTime); + } + + T GetTimeLeft() const + { + const auto now = std::chrono::steady_clock::now(); + + const auto left = ((m_startTime + m_totalWaitTime) - now); + + if (left < T::zero()) + return T::zero(); + + return std::chrono::duration_cast<T>(left); + } + + void SetExpired() { m_totalWaitTime = T::zero(); } + + void SetInfinite() { m_totalWaitTime = m_max; } + + T GetInitialTimeoutValue() const { return m_totalWaitTime; } + + std::chrono::steady_clock::time_point GetStartTime() const { return m_startTime; } + +private: + std::chrono::steady_clock::time_point m_startTime; + T m_totalWaitTime = T::zero(); + + static constexpr T m_max = + std::chrono::duration_cast<T>(std::chrono::steady_clock::duration::max()); +}; + +} // namespace XbmcThreads diff --git a/xbmc/threads/Thread.cpp b/xbmc/threads/Thread.cpp new file mode 100644 index 0000000..e23baa7 --- /dev/null +++ b/xbmc/threads/Thread.cpp @@ -0,0 +1,282 @@ +/* + * Copyright (c) 2002 Frodo + * Portions Copyright (c) by the authors of ffmpeg and xvid + * Copyright (C) 2002-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "Thread.h" + +#include "IRunnable.h" +#include "commons/Exception.h" +#include "threads/IThreadImpl.h" +#include "threads/SingleLock.h" +#include "utils/log.h" + +#include <atomic> +#include <inttypes.h> +#include <iostream> +#include <mutex> +#include <stdlib.h> + +static thread_local CThread* currentThread; + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + +CThread::CThread(const char* ThreadName) +: + m_bStop(false), m_StopEvent(true, true), m_StartEvent(true, true), m_pRunnable(nullptr) +{ + if (ThreadName) + m_ThreadName = ThreadName; +} + +CThread::CThread(IRunnable* pRunnable, const char* ThreadName) +: + m_bStop(false), m_StopEvent(true, true), m_StartEvent(true, true), m_pRunnable(pRunnable) +{ + if (ThreadName) + m_ThreadName = ThreadName; +} + +CThread::~CThread() +{ + StopThread(); + if (m_thread != nullptr) + { + m_thread->detach(); + delete m_thread; + } +} + +void CThread::Create(bool bAutoDelete) +{ + if (m_thread != nullptr) + { + // if the thread exited on it's own, without a call to StopThread, then we can get here + // incorrectly. We should be able to determine this by checking the promise. + std::future_status stat = m_future.wait_for(std::chrono::milliseconds(0)); + // a status of 'ready' means the future contains the value so the thread has exited + // since the thread can't exit without setting the future. + if (stat == std::future_status::ready) // this is an indication the thread has exited. + StopThread(true); // so let's just clean up + else + { // otherwise we have a problem. + CLog::Log(LOGERROR, "{} - fatal error creating thread {} - old thread id not null", + __FUNCTION__, m_ThreadName); + exit(1); + } + } + + m_bAutoDelete = bAutoDelete; + m_bStop = false; + m_StopEvent.Reset(); + m_StartEvent.Reset(); + + // lock? + //std::unique_lock<CCriticalSection> l(m_CriticalSection); + + std::promise<bool> prom; + m_future = prom.get_future(); + + { + // The std::thread internals must be set prior to the lambda doing + // any work. This will cause the lambda to wait until m_thread + // is fully initialized. Interestingly, using a std::atomic doesn't + // have the appropriate memory barrier behavior to accomplish the + // same thing so a full system mutex needs to be used. + std::unique_lock<CCriticalSection> blockLambdaTillDone(m_CriticalSection); + m_thread = new std::thread([](CThread* pThread, std::promise<bool> promise) + { + try + { + + { + // Wait for the pThread->m_thread internals to be set. Otherwise we could + // get to a place where we're reading, say, the thread id inside this + // lambda's call stack prior to the thread that kicked off this lambda + // having it set. Once this lock is released, the CThread::Create function + // that kicked this off is done so everything should be set. + std::unique_lock<CCriticalSection> waitForThreadInternalsToBeSet( + pThread->m_CriticalSection); + } + + // This is used in various helper methods like GetCurrentThread so it needs + // to be set before anything else is done. + currentThread = pThread; + + std::string name; + bool autodelete; + + if (pThread == nullptr) + { + CLog::Log(LOGERROR, "{}, sanity failed. thread is NULL.", __FUNCTION__); + promise.set_value(false); + return; + } + + name = pThread->m_ThreadName; + + std::stringstream ss; + ss << std::this_thread::get_id(); + std::string id = ss.str(); + autodelete = pThread->m_bAutoDelete; + + pThread->m_impl = IThreadImpl::CreateThreadImpl(pThread->m_thread->native_handle()); + pThread->m_impl->SetThreadInfo(name); + + CLog::Log(LOGDEBUG, "Thread {} start, auto delete: {}", name, + (autodelete ? "true" : "false")); + + pThread->m_StartEvent.Set(); + + pThread->Action(); + + if (autodelete) + { + CLog::Log(LOGDEBUG, "Thread {} {} terminating (autodelete)", name, id); + delete pThread; + pThread = NULL; + } + else + CLog::Log(LOGDEBUG, "Thread {} {} terminating", name, id); + } + catch (const std::exception& e) + { + CLog::Log(LOGDEBUG, "Thread Terminating with Exception: {}", e.what()); + } + catch (...) + { + CLog::Log(LOGDEBUG,"Thread Terminating with Exception"); + } + + promise.set_value(true); + }, this, std::move(prom)); + } // let the lambda proceed + + m_StartEvent.Wait(); // wait for the thread just spawned to set its internals +} + +bool CThread::IsRunning() const +{ + if (m_thread != nullptr) { + // it's possible that the thread exited on it's own without a call to StopThread. If so then + // the promise should be fulfilled. + std::future_status stat = m_future.wait_for(std::chrono::milliseconds(0)); + // a status of 'ready' means the future contains the value so the thread has exited + // since the thread can't exit without setting the future. + if (stat == std::future_status::ready) // this is an indication the thread has exited. + return false; + return true; // otherwise the thread is still active. + } else + return false; +} + +bool CThread::SetPriority(const ThreadPriority& priority) +{ + return m_impl->SetPriority(priority); +} + +bool CThread::IsAutoDelete() const +{ + return m_bAutoDelete; +} + +void CThread::StopThread(bool bWait /*= true*/) +{ + m_StartEvent.Wait(); + + m_bStop = true; + m_StopEvent.Set(); + std::unique_lock<CCriticalSection> lock(m_CriticalSection); + std::thread* lthread = m_thread; + if (lthread != nullptr && bWait && !IsCurrentThread()) + { + lock.unlock(); + if (!Join(std::chrono::milliseconds::max())) // eh? + lthread->join(); + m_thread = nullptr; + } +} + +void CThread::Process() +{ + if (m_pRunnable) + m_pRunnable->Run(); +} + +bool CThread::IsCurrentThread() const +{ + CThread* pThread = currentThread; + if (pThread != nullptr) + return pThread == this; + else + return false; +} + +CThread* CThread::GetCurrentThread() +{ + return currentThread; +} + +bool CThread::Join(std::chrono::milliseconds duration) +{ + std::unique_lock<CCriticalSection> l(m_CriticalSection); + std::thread* lthread = m_thread; + if (lthread != nullptr) + { + if (IsCurrentThread()) + return false; + + { + CSingleExit exit(m_CriticalSection); // don't hold the thread lock while we're waiting + std::future_status stat = m_future.wait_for(duration); + if (stat != std::future_status::ready) + return false; + } + + // it's possible it's already joined since we released the lock above. + if (lthread->joinable()) + m_thread->join(); + return true; + } + else + return false; +} + +void CThread::Action() +{ + try + { + OnStartup(); + } + catch (const XbmcCommons::UncheckedException &e) + { + e.LogThrowMessage("OnStartup"); + if (IsAutoDelete()) + return; + } + + try + { + Process(); + } + catch (const XbmcCommons::UncheckedException &e) + { + e.LogThrowMessage("Process"); + } + + try + { + OnExit(); + } + catch (const XbmcCommons::UncheckedException &e) + { + e.LogThrowMessage("OnExit"); + } +} diff --git a/xbmc/threads/Thread.h b/xbmc/threads/Thread.h new file mode 100644 index 0000000..c657d1b --- /dev/null +++ b/xbmc/threads/Thread.h @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +// Thread.h: interface for the CThread class. +// +////////////////////////////////////////////////////////////////////// + +#include "Event.h" + +#include <atomic> +#include <future> +#ifdef TARGET_DARWIN +#include <mach/mach.h> +#endif +#include <stdint.h> +#include <string> +#include <thread> + +enum class ThreadPriority +{ + LOWEST, + BELOW_NORMAL, + NORMAL, + ABOVE_NORMAL, + HIGHEST, + + /*! + * \brief Do not use this for priority. It is only needed to count the + * amount of values in the ThreadPriority enum. + * + */ + PRIORITY_COUNT, +}; + +class IRunnable; +class IThreadImpl; +class CThread +{ +protected: + explicit CThread(const char* ThreadName); + +public: + CThread(IRunnable* pRunnable, const char* ThreadName); + virtual ~CThread(); + void Create(bool bAutoDelete = false); + + template<typename Rep, typename Period> + void Sleep(std::chrono::duration<Rep, Period> duration) + { + if (duration > std::chrono::milliseconds(10) && IsCurrentThread()) + m_StopEvent.Wait(duration); + else + std::this_thread::sleep_for(duration); + } + + bool IsAutoDelete() const; + virtual void StopThread(bool bWait = true); + bool IsRunning() const; + + bool IsCurrentThread() const; + bool Join(std::chrono::milliseconds duration); + + inline static const std::thread::id GetCurrentThreadId() + { + return std::this_thread::get_id(); + } + + /*! + * \brief Set the threads priority. This uses the platforms + * native threading library to do so. + * + */ + bool SetPriority(const ThreadPriority& priority); + + static CThread* GetCurrentThread(); + + virtual void OnException(){} // signal termination handler + +protected: + virtual void OnStartup() {} + virtual void OnExit() {} + virtual void Process(); + + std::atomic<bool> m_bStop; + + enum WaitResponse { WAIT_INTERRUPTED = -1, WAIT_SIGNALED = 0, WAIT_TIMEDOUT = 1 }; + + /** + * This call will wait on a CEvent in an interruptible way such that if + * stop is called on the thread the wait will return with a response + * indicating what happened. + */ + inline WaitResponse AbortableWait(CEvent& event, + std::chrono::milliseconds duration = + std::chrono::milliseconds(-1) /* indicates wait forever*/) + { + XbmcThreads::CEventGroup group{&event, &m_StopEvent}; + const CEvent* result = + duration < std::chrono::milliseconds::zero() ? group.wait() : group.wait(duration); + return result == &event ? WAIT_SIGNALED : + (result == NULL ? WAIT_TIMEDOUT : WAIT_INTERRUPTED); + } + +private: + void Action(); + + bool m_bAutoDelete = false; + CEvent m_StopEvent; + CEvent m_StartEvent; + CCriticalSection m_CriticalSection; + IRunnable* m_pRunnable; + + std::string m_ThreadName; + std::thread* m_thread = nullptr; + std::future<bool> m_future; + + std::unique_ptr<IThreadImpl> m_impl; +}; diff --git a/xbmc/threads/Timer.cpp b/xbmc/threads/Timer.cpp new file mode 100644 index 0000000..d06ba40 --- /dev/null +++ b/xbmc/threads/Timer.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "Timer.h" + +#include <algorithm> + +using namespace std::chrono_literals; + +CTimer::CTimer(std::function<void()> const& callback) + : CThread("Timer"), m_callback(callback), m_timeout(0ms), m_interval(false) +{ } + +CTimer::CTimer(ITimerCallback *callback) + : CTimer(std::bind(&ITimerCallback::OnTimeout, callback)) +{ } + +CTimer::~CTimer() +{ + Stop(true); +} + +bool CTimer::Start(std::chrono::milliseconds timeout, bool interval /* = false */) +{ + if (m_callback == NULL || timeout == 0ms || IsRunning()) + return false; + + m_timeout = timeout; + m_interval = interval; + + Create(); + return true; +} + +bool CTimer::Stop(bool wait /* = false */) +{ + if (!IsRunning()) + return false; + + m_bStop = true; + m_eventTimeout.Set(); + StopThread(wait); + + return true; +} + +void CTimer::RestartAsync(std::chrono::milliseconds timeout) +{ + m_timeout = timeout; + m_endTime = std::chrono::steady_clock::now() + timeout; + m_eventTimeout.Set(); +} + +bool CTimer::Restart() +{ + if (!IsRunning()) + return false; + + Stop(true); + + return Start(m_timeout, m_interval); +} + +float CTimer::GetElapsedSeconds() const +{ + return GetElapsedMilliseconds() / 1000.0f; +} + +float CTimer::GetElapsedMilliseconds() const +{ + if (!IsRunning()) + return 0.0f; + + auto now = std::chrono::steady_clock::now(); + std::chrono::duration<float, std::milli> duration = (now - (m_endTime - m_timeout)); + + return duration.count(); +} + +void CTimer::Process() +{ + while (!m_bStop) + { + auto currentTime = std::chrono::steady_clock::now(); + m_endTime = currentTime + m_timeout; + + // wait the necessary time + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(m_endTime - currentTime); + + if (!m_eventTimeout.Wait(duration)) + { + currentTime = std::chrono::steady_clock::now(); + if (m_endTime <= currentTime) + { + // execute OnTimeout() callback + m_callback(); + + // continue if this is an interval timer, or if it was restarted during callback + if (!m_interval && m_endTime <= currentTime) + break; + } + } + } +} diff --git a/xbmc/threads/Timer.h b/xbmc/threads/Timer.h new file mode 100644 index 0000000..f434a38 --- /dev/null +++ b/xbmc/threads/Timer.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "Event.h" +#include "Thread.h" + +#include <chrono> +#include <functional> + +class ITimerCallback +{ +public: + virtual ~ITimerCallback() = default; + + virtual void OnTimeout() = 0; +}; + +class CTimer : protected CThread +{ +public: + explicit CTimer(ITimerCallback *callback); + explicit CTimer(std::function<void()> const& callback); + ~CTimer() override; + + bool Start(std::chrono::milliseconds timeout, bool interval = false); + bool Stop(bool wait = false); + bool Restart(); + void RestartAsync(std::chrono::milliseconds timeout); + + bool IsRunning() const { return CThread::IsRunning(); } + + float GetElapsedSeconds() const; + float GetElapsedMilliseconds() const; + +protected: + void Process() override; + +private: + std::function<void()> m_callback; + std::chrono::milliseconds m_timeout; + bool m_interval; + std::chrono::time_point<std::chrono::steady_clock> m_endTime; + CEvent m_eventTimeout; +}; diff --git a/xbmc/threads/test/CMakeLists.txt b/xbmc/threads/test/CMakeLists.txt new file mode 100644 index 0000000..136e972 --- /dev/null +++ b/xbmc/threads/test/CMakeLists.txt @@ -0,0 +1,7 @@ +set(SOURCES TestEvent.cpp + TestSharedSection.cpp + TestEndTime.cpp) + +set(HEADERS TestHelpers.h) + +core_add_test_library(threads_test) diff --git a/xbmc/threads/test/TestEndTime.cpp b/xbmc/threads/test/TestEndTime.cpp new file mode 100644 index 0000000..fb1c6d8 --- /dev/null +++ b/xbmc/threads/test/TestEndTime.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2005-2020 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "threads/SystemClock.h" + +#include <gtest/gtest.h> + +using namespace std::chrono_literals; + +namespace +{ + +template<typename T = std::chrono::milliseconds> +void CommonTests(XbmcThreads::EndTime<T>& endTime) +{ + EXPECT_EQ(100ms, endTime.GetInitialTimeoutValue()); + EXPECT_LT(T::zero(), endTime.GetStartTime().time_since_epoch()); + + EXPECT_FALSE(endTime.IsTimePast()); + EXPECT_LT(T::zero(), endTime.GetTimeLeft()); + + std::this_thread::sleep_for(100ms); + + EXPECT_TRUE(endTime.IsTimePast()); + EXPECT_EQ(T::zero(), endTime.GetTimeLeft()); + + endTime.SetInfinite(); + EXPECT_GE(T::max(), endTime.GetInitialTimeoutValue()); + EXPECT_EQ(XbmcThreads::EndTime<T>::Max(), endTime.GetInitialTimeoutValue()); + endTime.SetExpired(); + EXPECT_EQ(T::zero(), endTime.GetInitialTimeoutValue()); +} + +} // namespace + +TEST(TestEndTime, DefaultConstructor) +{ + XbmcThreads::EndTime<> endTime; + endTime.Set(100ms); + + CommonTests(endTime); +} + +TEST(TestEndTime, ExplicitConstructor) +{ + XbmcThreads::EndTime<> endTime(100ms); + + CommonTests(endTime); +} + +TEST(TestEndTime, DoubleMicroSeconds) +{ + XbmcThreads::EndTime<std::chrono::duration<double, std::micro>> endTime(100ms); + + CommonTests(endTime); +} + +TEST(TestEndTime, SteadyClockDuration) +{ + XbmcThreads::EndTime<std::chrono::steady_clock::duration> endTime(100ms); + + CommonTests(endTime); + + endTime.SetInfinite(); + EXPECT_EQ(std::chrono::steady_clock::duration::max(), endTime.GetInitialTimeoutValue()); + + endTime.SetExpired(); + EXPECT_EQ(std::chrono::steady_clock::duration::zero(), endTime.GetInitialTimeoutValue()); + + endTime.Set(endTime.Max()); + EXPECT_EQ(std::chrono::steady_clock::duration::max(), endTime.GetInitialTimeoutValue()); +} diff --git a/xbmc/threads/test/TestEvent.cpp b/xbmc/threads/test/TestEvent.cpp new file mode 100644 index 0000000..c4526b3 --- /dev/null +++ b/xbmc/threads/test/TestEvent.cpp @@ -0,0 +1,628 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "threads/Event.h" +#include "threads/IRunnable.h" +#include "threads/test/TestHelpers.h" + +#include <memory> +#include <stdio.h> + +using namespace XbmcThreads; +using namespace std::chrono_literals; + +//============================================================================= +// Helper classes +//============================================================================= + +class waiter : public IRunnable +{ + CEvent& event; +public: + bool& result; + + volatile bool waiting; + + waiter(CEvent& o, bool& flag) : event(o), result(flag), waiting(false) {} + + void Run() override + { + waiting = true; + result = event.Wait(); + waiting = false; + } +}; + +class timed_waiter : public IRunnable +{ + CEvent& event; + std::chrono::milliseconds waitTime; + +public: + int& result; + + volatile bool waiting; + + timed_waiter(CEvent& o, int& flag, std::chrono::milliseconds waitTimeMillis) + : event(o), waitTime(waitTimeMillis), result(flag), waiting(false) + { + } + + void Run() override + { + waiting = true; + result = 0; + result = event.Wait(waitTime) ? 1 : -1; + waiting = false; + } +}; + +class group_wait : public IRunnable +{ + CEventGroup& event; + std::chrono::milliseconds timeout; + +public: + CEvent* result; + bool waiting; + + group_wait(CEventGroup& o) : event(o), timeout(-1ms), result(NULL), waiting(false) {} + + group_wait(CEventGroup& o, std::chrono::milliseconds timeout_) + : event(o), timeout(timeout_), result(NULL), waiting(false) + { + } + + void Run() override + { + waiting = true; + if (timeout == -1ms) + result = event.wait(); + else + result = event.wait(timeout); + waiting = false; + } +}; + +//============================================================================= + +TEST(TestEvent, General) +{ + CEvent event; + bool result = false; + waiter w1(event,result); + thread waitThread(w1); + + EXPECT_TRUE(waitForWaiters(event, 1, 10000ms)); + + EXPECT_TRUE(!result); + + event.Set(); + + EXPECT_TRUE(waitThread.timed_join(10000ms)); + + EXPECT_TRUE(result); +} + +TEST(TestEvent, TwoWaits) +{ + CEvent event; + bool result1 = false; + bool result2 = false; + waiter w1(event,result1); + waiter w2(event,result2); + thread waitThread1(w1); + thread waitThread2(w2); + + EXPECT_TRUE(waitForWaiters(event, 2, 10000ms)); + + EXPECT_TRUE(!result1); + EXPECT_TRUE(!result2); + + event.Set(); + + EXPECT_TRUE(waitThread1.timed_join(10000ms)); + EXPECT_TRUE(waitThread2.timed_join(10000ms)); + + EXPECT_TRUE(result1); + EXPECT_TRUE(result2); + +} + +TEST(TestEvent, TimedWaits) +{ + CEvent event; + int result1 = 10; + timed_waiter w1(event, result1, 100ms); + thread waitThread1(w1); + + EXPECT_TRUE(waitForWaiters(event, 1, 10000ms)); + + EXPECT_TRUE(result1 == 0); + + event.Set(); + + EXPECT_TRUE(waitThread1.timed_join(10000ms)); + + EXPECT_TRUE(result1 == 1); +} + +TEST(TestEvent, TimedWaitsTimeout) +{ + CEvent event; + int result1 = 10; + timed_waiter w1(event, result1, 50ms); + thread waitThread1(w1); + + EXPECT_TRUE(waitForWaiters(event, 1, 100ms)); + + EXPECT_TRUE(result1 == 0); + + EXPECT_TRUE(waitThread1.timed_join(10000ms)); + + EXPECT_TRUE(result1 == -1); +} + +TEST(TestEvent, Group) +{ + CEvent event1; + CEvent event2; + + CEventGroup group{&event1,&event2}; + + bool result1 = false; + bool result2 = false; + + waiter w1(event1,result1); + waiter w2(event2,result2); + group_wait w3(group); + + thread waitThread1(w1); + thread waitThread2(w2); + thread waitThread3(w3); + + EXPECT_TRUE(waitForWaiters(event1, 1, 10000ms)); + EXPECT_TRUE(waitForWaiters(event2, 1, 10000ms)); + EXPECT_TRUE(waitForWaiters(group, 1, 10000ms)); + + EXPECT_TRUE(!result1); + EXPECT_TRUE(!result2); + + EXPECT_TRUE(w3.waiting); + EXPECT_TRUE(w3.result == NULL); + + event1.Set(); + + EXPECT_TRUE(waitThread1.timed_join(10000ms)); + EXPECT_TRUE(waitThread3.timed_join(10000ms)); + std::this_thread::sleep_for(10ms); + + EXPECT_TRUE(result1); + EXPECT_TRUE(!w1.waiting); + EXPECT_TRUE(!result2); + EXPECT_TRUE(w2.waiting); + EXPECT_TRUE(!w3.waiting); + EXPECT_TRUE(w3.result == &event1); + + event2.Set(); + + EXPECT_TRUE(waitThread2.timed_join(10000ms)); +} + +/* Test disabled for now, because it deadlocks +TEST(TestEvent, GroupLimitedGroupScope) +{ + CEvent event1; + CEvent event2; + + { + CEventGroup group(&event1,&event2,NULL); + + bool result1 = false; + bool result2 = false; + + waiter w1(event1,result1); + waiter w2(event2,result2); + group_wait w3(group); + + thread waitThread1(w1); + thread waitThread2(w2); + thread waitThread3(w3); + + EXPECT_TRUE(waitForWaiters(event1,1,10000ms)); + EXPECT_TRUE(waitForWaiters(event2,1,10000ms)); + EXPECT_TRUE(waitForWaiters(group,1,10000ms)); + + EXPECT_TRUE(!result1); + EXPECT_TRUE(!result2); + + EXPECT_TRUE(w3.waiting); + EXPECT_TRUE(w3.result == NULL); + + event1.Set(); + + EXPECT_TRUE(waitThread1.timed_join(10000ms)); + EXPECT_TRUE(waitThread3.timed_join(10000ms)); + std::this_thread::sleep_for(10ms); + + EXPECT_TRUE(result1); + EXPECT_TRUE(!w1.waiting); + EXPECT_TRUE(!result2); + EXPECT_TRUE(w2.waiting); + EXPECT_TRUE(!w3.waiting); + EXPECT_TRUE(w3.result == &event1); + } + + event2.Set(); + + std::this_thread::sleep_for(50ms); // give thread 2 a chance to exit +}*/ + +TEST(TestEvent, TwoGroups) +{ + CEvent event1; + CEvent event2; + + CEventGroup group1{&event1,&event2}; + CEventGroup group2{&event1,&event2}; + + bool result1 = false; + bool result2 = false; + + waiter w1(event1,result1); + waiter w2(event2,result2); + group_wait w3(group1); + group_wait w4(group2); + + thread waitThread1(w1); + thread waitThread2(w2); + thread waitThread3(w3); + thread waitThread4(w4); + + EXPECT_TRUE(waitForWaiters(event1, 1, 10000ms)); + EXPECT_TRUE(waitForWaiters(event2, 1, 10000ms)); + EXPECT_TRUE(waitForWaiters(group1, 1, 10000ms)); + EXPECT_TRUE(waitForWaiters(group2, 1, 10000ms)); + + EXPECT_TRUE(!result1); + EXPECT_TRUE(!result2); + + EXPECT_TRUE(w3.waiting); + EXPECT_EQ(w3.result,(void*)NULL); + EXPECT_TRUE(w4.waiting); + EXPECT_EQ(w4.result,(void*)NULL); + + event1.Set(); + + EXPECT_TRUE(waitThread1.timed_join(10000ms)); + EXPECT_TRUE(waitThread3.timed_join(10000ms)); + EXPECT_TRUE(waitThread4.timed_join(10000ms)); + std::this_thread::sleep_for(10ms); + + EXPECT_TRUE(result1); + EXPECT_TRUE(!w1.waiting); + EXPECT_TRUE(!result2); + EXPECT_TRUE(w2.waiting); + EXPECT_TRUE(!w3.waiting); + EXPECT_TRUE(w3.result == &event1); + EXPECT_TRUE(!w4.waiting); + EXPECT_TRUE(w4.result == &event1); + + event2.Set(); + + EXPECT_TRUE(waitThread2.timed_join(10000ms)); +} + +TEST(TestEvent, AutoResetBehavior) +{ + CEvent event; + + EXPECT_TRUE(!event.Wait(1ms)); + + event.Set(); // event will remain signaled if there are no waits + + EXPECT_TRUE(event.Wait(1ms)); +} + +TEST(TestEvent, ManualReset) +{ + CEvent event(true); + bool result = false; + waiter w1(event,result); + thread waitThread(w1); + + EXPECT_TRUE(waitForWaiters(event, 1, 10000ms)); + + EXPECT_TRUE(!result); + + event.Set(); + + EXPECT_TRUE(waitThread.timed_join(10000ms)); + + EXPECT_TRUE(result); + + // with manual reset, the state should remain signaled + EXPECT_TRUE(event.Wait(1ms)); + + event.Reset(); + + EXPECT_TRUE(!event.Wait(1ms)); +} + +TEST(TestEvent, InitVal) +{ + CEvent event(false,true); + EXPECT_TRUE(event.Wait(50ms)); +} + +TEST(TestEvent, SimpleTimeout) +{ + CEvent event; + EXPECT_TRUE(!event.Wait(50ms)); +} + +TEST(TestEvent, GroupChildSet) +{ + CEvent event1(true); + CEvent event2; + + event1.Set(); + CEventGroup group{&event1,&event2}; + + bool result1 = false; + bool result2 = false; + + waiter w1(event1,result1); + waiter w2(event2,result2); + group_wait w3(group); + + thread waitThread1(w1); + thread waitThread2(w2); + thread waitThread3(w3); + + EXPECT_TRUE(waitForWaiters(event2, 1, 10000ms)); + EXPECT_TRUE(waitThread1.timed_join(10000ms)); + EXPECT_TRUE(waitThread3.timed_join(10000ms)); + std::this_thread::sleep_for(10ms); + + EXPECT_TRUE(result1); + EXPECT_TRUE(!result2); + + EXPECT_TRUE(!w3.waiting); + EXPECT_TRUE(w3.result == &event1); + + event2.Set(); + + EXPECT_TRUE(waitThread2.timed_join(10000ms)); +} + +TEST(TestEvent, GroupChildSet2) +{ + CEvent event1(true,true); + CEvent event2; + + CEventGroup group{&event1,&event2}; + + bool result1 = false; + bool result2 = false; + + waiter w1(event1,result1); + waiter w2(event2,result2); + group_wait w3(group); + + thread waitThread1(w1); + thread waitThread2(w2); + thread waitThread3(w3); + + EXPECT_TRUE(waitForWaiters(event2, 1, 10000ms)); + EXPECT_TRUE(waitThread1.timed_join(10000ms)); + EXPECT_TRUE(waitThread3.timed_join(10000ms)); + std::this_thread::sleep_for(10ms); + + EXPECT_TRUE(result1); + EXPECT_TRUE(!result2); + + EXPECT_TRUE(!w3.waiting); + EXPECT_TRUE(w3.result == &event1); + + event2.Set(); + + EXPECT_TRUE(waitThread2.timed_join(10000ms)); +} + +TEST(TestEvent, GroupWaitResetsChild) +{ + CEvent event1; + CEvent event2; + + CEventGroup group{&event1,&event2}; + + group_wait w3(group); + + thread waitThread3(w3); + + EXPECT_TRUE(waitForWaiters(group, 1, 10000ms)); + + EXPECT_TRUE(w3.waiting); + EXPECT_TRUE(w3.result == NULL); + + event2.Set(); + + EXPECT_TRUE(waitThread3.timed_join(10000ms)); + + EXPECT_TRUE(!w3.waiting); + EXPECT_TRUE(w3.result == &event2); + // event2 should have been reset. + EXPECT_TRUE(event2.Wait(1ms) == false); +} + +TEST(TestEvent, GroupTimedWait) +{ + CEvent event1; + CEvent event2; + CEventGroup group{&event1,&event2}; + + bool result1 = false; + bool result2 = false; + + waiter w1(event1,result1); + waiter w2(event2,result2); + + thread waitThread1(w1); + thread waitThread2(w2); + + EXPECT_TRUE(waitForWaiters(event1, 1, 10000ms)); + EXPECT_TRUE(waitForWaiters(event2, 1, 10000ms)); + + EXPECT_TRUE(group.wait(20ms) == NULL); // waited ... got nothing + + group_wait w3(group, 50ms); + thread waitThread3(w3); + + EXPECT_TRUE(waitForWaiters(group, 1, 10000ms)); + + EXPECT_TRUE(!result1); + EXPECT_TRUE(!result2); + + EXPECT_TRUE(w3.waiting); + EXPECT_TRUE(w3.result == NULL); + + // this should end given the wait is for only 50 millis + EXPECT_TRUE(waitThread3.timed_join(200ms)); + + EXPECT_TRUE(!w3.waiting); + EXPECT_TRUE(w3.result == NULL); + + group_wait w4(group, 50ms); + thread waitThread4(w4); + + EXPECT_TRUE(waitForWaiters(group, 1, 10000ms)); + + EXPECT_TRUE(w4.waiting); + EXPECT_TRUE(w4.result == NULL); + + event1.Set(); + + EXPECT_TRUE(waitThread1.timed_join(10000ms)); + EXPECT_TRUE(waitThread4.timed_join(10000ms)); + std::this_thread::sleep_for(10ms); + + EXPECT_TRUE(result1); + EXPECT_TRUE(!result2); + + EXPECT_TRUE(!w4.waiting); + EXPECT_TRUE(w4.result == &event1); + + event2.Set(); + + EXPECT_TRUE(waitThread2.timed_join(10000ms)); +} + +#define TESTNUM 100000l +#define NUMTHREADS 100l + +CEvent* g_event = NULL; +std::atomic<long> g_mutex; + +class mass_waiter : public IRunnable +{ +public: + CEvent& event; + bool result; + + volatile bool waiting = false; + + mass_waiter() : event(*g_event) {} + + void Run() override + { + waiting = true; + AtomicGuard g(&g_mutex); + result = event.Wait(); + waiting = false; + } +}; + +class poll_mass_waiter : public IRunnable +{ +public: + CEvent& event; + bool result; + + volatile bool waiting = false; + + poll_mass_waiter() : event(*g_event) {} + + void Run() override + { + waiting = true; + AtomicGuard g(&g_mutex); + while ((result = event.Wait(0ms)) == false) + ; + waiting = false; + } +}; + +template <class W> void RunMassEventTest(std::vector<std::shared_ptr<W>>& m, bool canWaitOnEvent) +{ + std::vector<std::shared_ptr<thread>> t(NUMTHREADS); + for(size_t i=0; i<NUMTHREADS; i++) + t[i].reset(new thread(*m[i])); + + EXPECT_TRUE(waitForThread(g_mutex, NUMTHREADS, 10000ms)); + if (canWaitOnEvent) + { + EXPECT_TRUE(waitForWaiters(*g_event, NUMTHREADS, 10000ms)); + } + + std::this_thread::sleep_for(100ms); // give them a little more time + + for(size_t i=0; i<NUMTHREADS; i++) + { + EXPECT_TRUE(m[i]->waiting); + } + + g_event->Set(); + + for(size_t i=0; i<NUMTHREADS; i++) + { + EXPECT_TRUE(t[i]->timed_join(10000ms)); + } + + for(size_t i=0; i<NUMTHREADS; i++) + { + EXPECT_TRUE(!m[i]->waiting); + EXPECT_TRUE(m[i]->result); + } +} + + +TEST(TestMassEvent, General) +{ + g_event = new CEvent(); + + std::vector<std::shared_ptr<mass_waiter>> m(NUMTHREADS); + for(size_t i=0; i<NUMTHREADS; i++) + m[i].reset(new mass_waiter()); + + RunMassEventTest(m,true); + delete g_event; +} + +TEST(TestMassEvent, Polling) +{ + g_event = new CEvent(true); // polling needs to avoid the auto-reset + + std::vector<std::shared_ptr<poll_mass_waiter>> m(NUMTHREADS); + for(size_t i=0; i<NUMTHREADS; i++) + m[i].reset(new poll_mass_waiter()); + + RunMassEventTest(m,false); + delete g_event; +} + diff --git a/xbmc/threads/test/TestHelpers.h b/xbmc/threads/test/TestHelpers.h new file mode 100644 index 0000000..4d8752c --- /dev/null +++ b/xbmc/threads/test/TestHelpers.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/Thread.h" + +#include <memory> +#include <mutex> + +#include <gtest/gtest.h> + +template<class E> +inline static bool waitForWaiters(E& event, int numWaiters, std::chrono::milliseconds duration) +{ + for (auto i = std::chrono::milliseconds::zero(); i < duration; i++) + { + if (event.getNumWaits() == numWaiters) + return true; + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + return false; +} + +inline static bool waitForThread(std::atomic<long>& mutex, + int numWaiters, + std::chrono::milliseconds duration) +{ + CCriticalSection sec; + for (auto i = std::chrono::milliseconds::zero(); i < duration; i++) + { + if (mutex == (long)numWaiters) + return true; + + { + std::unique_lock<CCriticalSection> tmplock(sec); // kick any memory syncs + } + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + return false; +} + +class AtomicGuard +{ + std::atomic<long>* val; +public: + inline AtomicGuard(std::atomic<long>* val_) : val(val_) { if (val) ++(*val); } + inline ~AtomicGuard() { if (val) --(*val); } +}; + +class thread +{ + std::unique_ptr<CThread> cthread; + +public: + inline explicit thread(IRunnable& runnable) + : cthread(std::make_unique<CThread>(&runnable, "DumbThread")) + { + cthread->Create(); + } + + void join() { cthread->Join(std::chrono::milliseconds::max()); } + + bool timed_join(std::chrono::milliseconds duration) { return cthread->Join(duration); } +}; + diff --git a/xbmc/threads/test/TestSharedSection.cpp b/xbmc/threads/test/TestSharedSection.cpp new file mode 100644 index 0000000..fc2ca08 --- /dev/null +++ b/xbmc/threads/test/TestSharedSection.cpp @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "threads/Event.h" +#include "threads/IRunnable.h" +#include "threads/SharedSection.h" +#include "threads/test/TestHelpers.h" + +#include <mutex> +#include <shared_mutex> +#include <stdio.h> + +using namespace std::chrono_literals; + +//============================================================================= +// Helper classes +//============================================================================= + +template<class L> +class locker : public IRunnable +{ + CSharedSection& sec; + CEvent* wait; + + std::atomic<long>* mutex; +public: + volatile bool haslock; + volatile bool obtainedlock; + + inline locker(CSharedSection& o, std::atomic<long>* mutex_ = NULL, CEvent* wait_ = NULL) : + sec(o), wait(wait_), mutex(mutex_), haslock(false), obtainedlock(false) {} + + inline locker(CSharedSection& o, CEvent* wait_ = NULL) : + sec(o), wait(wait_), mutex(NULL), haslock(false), obtainedlock(false) {} + + void Run() override + { + AtomicGuard g(mutex); + L lock(sec); + haslock = true; + obtainedlock = true; + if (wait) + wait->Wait(); + haslock = false; + } +}; + +TEST(TestCritSection, General) +{ + CCriticalSection sec; + + std::unique_lock<CCriticalSection> l1(sec); + std::unique_lock<CCriticalSection> l2(sec); +} + +TEST(TestSharedSection, General) +{ + CSharedSection sec; + + std::shared_lock<CSharedSection> l1(sec); + std::shared_lock<CSharedSection> l2(sec); +} + +TEST(TestSharedSection, GetSharedLockWhileTryingExclusiveLock) +{ + std::atomic<long> mutex(0L); + CEvent event; + + CSharedSection sec; + + std::shared_lock<CSharedSection> l1(sec); // get a shared lock + + locker<std::unique_lock<CSharedSection>> l2(sec, &mutex); + thread waitThread1(l2); // try to get an exclusive lock + + EXPECT_TRUE(waitForThread(mutex, 1, 10000ms)); + std::this_thread::sleep_for(10ms); // still need to give it a chance to move ahead + + EXPECT_TRUE(!l2.haslock); // this thread is waiting ... + EXPECT_TRUE(!l2.obtainedlock); // this thread is waiting ... + + // now try and get a SharedLock + locker<std::shared_lock<CSharedSection>> l3(sec, &mutex, &event); + thread waitThread3(l3); // try to get a shared lock + EXPECT_TRUE(waitForThread(mutex, 2, 10000ms)); + std::this_thread::sleep_for(10ms); + EXPECT_TRUE(l3.haslock); + + event.Set(); + EXPECT_TRUE(waitThread3.timed_join(10000ms)); + + // l3 should have released. + EXPECT_TRUE(!l3.haslock); + + // but the exclusive lock should still not have happened + EXPECT_TRUE(!l2.haslock); // this thread is waiting ... + EXPECT_TRUE(!l2.obtainedlock); // this thread is waiting ... + + // let it go + l1.unlock(); // the last shared lock leaves. + + EXPECT_TRUE(waitThread1.timed_join(10000ms)); + + EXPECT_TRUE(l2.obtainedlock); // the exclusive lock was captured + EXPECT_TRUE(!l2.haslock); // ... but it doesn't have it anymore +} + +TEST(TestSharedSection, TwoCase) +{ + CSharedSection sec; + + CEvent event; + std::atomic<long> mutex(0L); + + locker<std::shared_lock<CSharedSection>> l1(sec, &mutex, &event); + + { + std::shared_lock<CSharedSection> lock(sec); + thread waitThread1(l1); + + EXPECT_TRUE(waitForWaiters(event, 1, 10000ms)); + EXPECT_TRUE(l1.haslock); + + event.Set(); + + EXPECT_TRUE(waitThread1.timed_join(10000ms)); + } + + locker<std::shared_lock<CSharedSection>> l2(sec, &mutex, &event); + { + std::unique_lock<CSharedSection> lock(sec); // get exclusive lock + thread waitThread2(l2); // thread should block + + EXPECT_TRUE(waitForThread(mutex, 1, 10000ms)); + std::this_thread::sleep_for(10ms); + + EXPECT_TRUE(!l2.haslock); + + lock.unlock(); + + EXPECT_TRUE(waitForWaiters(event, 1, 10000ms)); + std::this_thread::sleep_for(10ms); + EXPECT_TRUE(l2.haslock); + + event.Set(); + + EXPECT_TRUE(waitThread2.timed_join(10000ms)); + } +} + +TEST(TestMultipleSharedSection, General) +{ + CSharedSection sec; + + CEvent event; + std::atomic<long> mutex(0L); + + locker<std::shared_lock<CSharedSection>> l1(sec, &mutex, &event); + + { + std::shared_lock<CSharedSection> lock(sec); + thread waitThread1(l1); + + EXPECT_TRUE(waitForThread(mutex, 1, 10000ms)); + std::this_thread::sleep_for(10ms); + + EXPECT_TRUE(l1.haslock); + + event.Set(); + + EXPECT_TRUE(waitThread1.timed_join(10000ms)); + } + + locker<std::shared_lock<CSharedSection>> l2(sec, &mutex, &event); + locker<std::shared_lock<CSharedSection>> l3(sec, &mutex, &event); + locker<std::shared_lock<CSharedSection>> l4(sec, &mutex, &event); + locker<std::shared_lock<CSharedSection>> l5(sec, &mutex, &event); + { + std::unique_lock<CSharedSection> lock(sec); + thread waitThread1(l2); + thread waitThread2(l3); + thread waitThread3(l4); + thread waitThread4(l5); + + EXPECT_TRUE(waitForThread(mutex, 4, 10000ms)); + std::this_thread::sleep_for(10ms); + + EXPECT_TRUE(!l2.haslock); + EXPECT_TRUE(!l3.haslock); + EXPECT_TRUE(!l4.haslock); + EXPECT_TRUE(!l5.haslock); + + lock.unlock(); + + EXPECT_TRUE(waitForWaiters(event, 4, 10000ms)); + + EXPECT_TRUE(l2.haslock); + EXPECT_TRUE(l3.haslock); + EXPECT_TRUE(l4.haslock); + EXPECT_TRUE(l5.haslock); + + event.Set(); + + EXPECT_TRUE(waitThread1.timed_join(10000ms)); + EXPECT_TRUE(waitThread2.timed_join(10000ms)); + EXPECT_TRUE(waitThread3.timed_join(10000ms)); + EXPECT_TRUE(waitThread4.timed_join(10000ms)); + } +} + |