diff options
author | Alwin Esch <alwin.esch@web.de> | 2020-10-04 13:19:40 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-04 13:19:40 +0200 |
commit | 6507e435983bba3369e69606b41e844bf989a4f1 (patch) | |
tree | 65c141b6d5f18ff274aea14d5a7b08e015b450fa | |
parent | cccae01fd5a8f08b661c4fde5663c02a629f975e (diff) | |
parent | 84c8aadb9e4b8e8d487b9d984f03da0818d1df8d (diff) |
Merge pull request #18507 from AlwinEsch/add-thread-to-dev-kit
[addons][tools] add thread helper classes to addon headers
4 files changed, 718 insertions, 2 deletions
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/tools/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/tools/CMakeLists.txt index 63f1c2c3db..517ea9308e 100644 --- a/xbmc/addons/kodi-dev-kit/include/kodi/tools/CMakeLists.txt +++ b/xbmc/addons/kodi-dev-kit/include/kodi/tools/CMakeLists.txt @@ -1,5 +1,7 @@ set(HEADERS DllHelper.h - StringUtils.h) + StringUtils.h + Thread.h + Timer.h) if(NOT ENABLE_STATIC_LIBS) core_add_library(addons_kodi-dev-kit_include_kodi_tools) diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/tools/Thread.h b/xbmc/addons/kodi-dev-kit/include/kodi/tools/Thread.h new file mode 100644 index 0000000000..c888efbb59 --- /dev/null +++ b/xbmc/addons/kodi-dev-kit/include/kodi/tools/Thread.h @@ -0,0 +1,399 @@ +/* + * 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. + */ + +#pragma once + +#ifdef __cplusplus + +#include "../General.h" + +#include <chrono> +#include <condition_variable> +#include <future> +#include <mutex> +#include <thread> + +namespace kodi +{ +namespace tools +{ + +//============================================================================== +/// @defgroup cpp_kodi_tools_CThread class CThread +/// @ingroup cpp_kodi_tools +/// @brief **Helper class to represent threads of execution**\n +/// An execution thread is a sequence of instructions that can run concurrently +/// with other such sequences in multithreaded environments while sharing the +/// same address space. +/// +/// Is intended to reduce any code work of C++ on addons and to have them faster +/// to use. +/// +/// His code uses the support of platform-independent thread system introduced +/// with C++11. +/// +/// ---------------------------------------------------------------------------- +/// +/// **Example:** +/// ~~~~~~~~~~~~~{.cpp} +/// #include <kodi/tools/Thread.h> +/// #include <kodi/AddonBase.h> +/// +/// class ATTRIBUTE_HIDDEN CTestAddon +/// : public kodi::addon::CAddonBase, +/// public kodi::tools::CThread +/// { +/// public: +/// CTestAddon() = default; +/// +/// ADDON_STATUS Create() override; +/// +/// void Process() override; +/// }; +/// +/// ADDON_STATUS CTestAddon::Create() +/// { +/// kodi::Log(ADDON_LOG_INFO, "Starting thread"); +/// CreateThread(); +/// +/// Sleep(4000); +/// +/// kodi::Log(ADDON_LOG_INFO, "Stopping thread"); +/// // This added as example and also becomes stopped by class destructor +/// StopThread(); +/// +/// return ADDON_STATUS_OK; +/// } +/// +/// void CTestAddon::Process() +/// { +/// kodi::Log(ADDON_LOG_INFO, "Thread started"); +/// +/// while (!m_threadStop) +/// { +/// kodi::Log(ADDON_LOG_INFO, "Hello World"); +/// Sleep(1000); +/// } +/// +/// kodi::Log(ADDON_LOG_INFO, "Thread ended"); +/// } +/// +/// ADDONCREATOR(CTestAddon) +/// ~~~~~~~~~~~~~ +/// +///@{ +class CThread +{ +public: + //============================================================================ + /// @ingroup cpp_kodi_tools_CThread + /// @brief Class constructor. + /// + CThread() : m_threadStop(false) {} + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CThread + /// @brief Class destructor. + /// + virtual ~CThread() + { + StopThread(); + if (m_thread != nullptr) + { + m_thread->detach(); + delete m_thread; + } + } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CThread + /// @brief Check auto delete is enabled on this thread class. + /// + /// @return true if auto delete is used, false otherwise + /// + bool IsAutoDelete() const { return m_autoDelete; } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CThread + /// @brief Check caller is on this running thread. + /// + /// @return true if called from thread inside the class, false if from another + /// thread + /// + bool IsCurrentThread() const { return m_threadId == std::this_thread::get_id(); } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CThread + /// @brief Check thread inside this class is running and active. + /// + /// @note This function should be used from outside and not within process to + /// check thread is active. Use use atomic bool @ref m_threadStop for this. + /// + /// @return true if running, false if not + /// + bool 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; + } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CThread + /// @brief Create a new thread defined by this class on child. + /// + /// This starts then @ref Process() where is available on the child by addon. + /// + /// @param[in] autoDelete To set thread to delete itself after end, default is + /// false + /// + void CreateThread(bool autoDelete = false) + { + 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. + kodi::Log(ADDON_LOG_FATAL, "%s - fatal error creating thread - old thread id not null", + __func__); + exit(1); + } + } + + m_autoDelete = autoDelete; + m_threadStop = false; + m_startEvent.notify_all(); + m_stopEvent.notify_all(); + + 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<std::recursive_mutex> lock(m_threadMutex); + m_thread = new std::thread( + [](CThread* thread, 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<std::recursive_mutex> lock(thread->m_threadMutex); + } + + thread->m_threadId = std::this_thread::get_id(); + std::stringstream ss; + ss << thread->m_threadId; + std::string id = ss.str(); + bool autodelete = thread->m_autoDelete; + + kodi::Log(ADDON_LOG_DEBUG, "Thread %s start, auto delete: %s", id.c_str(), + (autodelete ? "true" : "false")); + + thread->m_running = true; + thread->m_startEvent.notify_one(); + + thread->Process(); + + if (autodelete) + { + kodi::Log(ADDON_LOG_DEBUG, "Thread %s terminating (autodelete)", id.c_str()); + delete thread; + thread = nullptr; + } + else + kodi::Log(ADDON_LOG_DEBUG, "Thread %s terminating", id.c_str()); + } + catch (const std::exception& e) + { + kodi::Log(ADDON_LOG_DEBUG, "Thread Terminating with Exception: %s", e.what()); + } + catch (...) + { + kodi::Log(ADDON_LOG_DEBUG, "Thread Terminating with Exception"); + } + + promise.set_value(true); + }, + this, std::move(prom)); + + m_startEvent.wait(lock); + } + } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CThread + /// @brief Stop a running thread. + /// + /// @param[in] wait As true (default) to wait until thread is finished and + /// stopped, as false the function return directly and thread + /// becomes independently stopped. + /// + void StopThread(bool wait = true) + { + std::unique_lock<std::recursive_mutex> lock(m_threadMutex); + + if (m_threadStop) + return; + + if (!m_running) + m_startEvent.wait(lock); + m_running = false; + m_threadStop = true; + m_stopEvent.notify_one(); + + std::thread* lthread = m_thread; + if (lthread != nullptr && wait && !IsCurrentThread()) + { + lock.unlock(); + if (lthread->joinable()) + lthread->join(); + delete m_thread; + m_thread = nullptr; + m_threadId = std::thread::id(); + } + } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CThread + /// @brief Thread sleep with given amount of milliseconds. + /// + /// This makes a sleep in the thread with a given time value. If it is called + /// within the process itself, it is also checked whether the thread is + /// terminated and the sleep process is thereby interrupted. + /// + /// If the external point calls this, only a regular sleep is used, which runs + /// through completely. + /// + /// @param[in] milliseconds Time to sleep + /// + void Sleep(uint32_t milliseconds) + { + if (milliseconds > 10 && IsCurrentThread()) + { + std::unique_lock<std::recursive_mutex> lock(m_threadMutex); + m_stopEvent.wait_for(lock, std::chrono::milliseconds(milliseconds)); + } + else + { + std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); + } + } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CThread + /// @brief The function returns when the thread execution has completed or + /// timing is reached in milliseconds beforehand + /// + /// This synchronizes the moment this function returns with the completion of + /// all operations on the thread. + /// + /// @param[in] milliseconds Time to wait for join + /// + bool Join(unsigned int milliseconds) + { + std::unique_lock<std::recursive_mutex> lock(m_threadMutex); + std::thread* lthread = m_thread; + if (lthread != nullptr) + { + if (IsCurrentThread()) + return false; + + { + m_threadMutex.unlock(); // don't hold the thread lock while we're waiting + std::future_status stat = m_future.wait_for(std::chrono::milliseconds(milliseconds)); + if (stat != std::future_status::ready) + return false; + m_threadMutex.lock(); + } + + // it's possible it's already joined since we released the lock above. + if (lthread->joinable()) + m_thread->join(); + return true; + } + else + return false; + } + //---------------------------------------------------------------------------- + +protected: + //============================================================================ + /// @ingroup cpp_kodi_tools_CThread + /// @brief The function to be added by the addon as a child to carry out the + /// process thread. + /// + /// Use @ref m_threadStop to check about active of thread and want stopped from + /// external place. + /// + /// @note This function is necessary and must be implemented by the addon. + /// + virtual void Process() = 0; + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CThread + /// @brief Atomic bool to indicate thread is active. + /// + /// This should be used in @ref Process() to check the activity of the thread and, + /// if true, to terminate the process. + /// + /// - <b>`false`</b>: Thread active and should be run + /// - <b>`true`</b>: Thread ends and should be stopped + /// + std::atomic<bool> m_threadStop; + //---------------------------------------------------------------------------- + +private: + bool m_autoDelete = false; + bool m_running = false; + std::condition_variable_any m_stopEvent; + std::condition_variable_any m_startEvent; + std::recursive_mutex m_threadMutex; + std::thread::id m_threadId; + std::thread* m_thread = nullptr; + std::future<bool> m_future; +}; +///@} +//------------------------------------------------------------------------------ + +} /* namespace tools */ +} /* namespace kodi */ + +#endif /* __cplusplus */ diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/tools/Timer.h b/xbmc/addons/kodi-dev-kit/include/kodi/tools/Timer.h new file mode 100644 index 0000000000..c1f9dd800a --- /dev/null +++ b/xbmc/addons/kodi-dev-kit/include/kodi/tools/Timer.h @@ -0,0 +1,315 @@ +/* + * 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. + */ + +#pragma once + +#ifdef __cplusplus + +#include "Thread.h" + +#include <functional> + +namespace kodi +{ +namespace tools +{ + +//============================================================================== +/// @defgroup cpp_kodi_tools_CTimer class CTimer +/// @ingroup cpp_kodi_tools +/// @brief **Time interval management**\n +/// Class which enables a time interval to be called up by a given function or +/// class by means of a thread. +/// +/// His code uses the support of platform-independent thread system introduced +/// with C++11. +/// +/// +/// ---------------------------------------------------------------------------- +/// +/// **Example:** +/// ~~~~~~~~~~~~~{.cpp} +/// #include <kodi/tools/Timer.h> +/// +/// class ATTRIBUTE_HIDDEN CExample +/// { +/// public: +/// CExample() : m_timer([this](){TimerCall();}) +/// { +/// m_timer.Start(5000, true); // let call continuously all 5 seconds +/// } +/// +/// void TimerCall() +/// { +/// fprintf(stderr, "Hello World\n"); +/// } +/// +/// private: +/// kodi::tools::CTimer m_timer; +/// }; +/// ~~~~~~~~~~~~~ +/// +///@{ +class CTimer : protected CThread +{ +public: + class ITimerCallback; + + //============================================================================ + /// @ingroup cpp_kodi_tools_CTimer + /// @brief Class constructor to pass individual other class as callback. + /// + /// @param[in] callback Child class of parent @ref ITimerCallback with + /// implemented function @ref ITimerCallback::OnTimeout(). + /// + explicit CTimer(kodi::tools::CTimer::ITimerCallback* callback) + : CTimer(std::bind(&ITimerCallback::OnTimeout, callback)) + { + } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CTimer + /// @brief Class constructor to pass individual function as callback. + /// + /// @param[in] callback Function to pass as callback about timeout. + /// + /// **Callback function style:** + /// ~~~~~~~~~~~~~{.cpp} + /// void TimerCallback() + /// { + /// } + /// ~~~~~~~~~~~~~ + explicit CTimer(std::function<void()> const& callback) : m_callback(callback) {} + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CTimer + /// @brief Class destructor. + /// + ~CTimer() override { Stop(true); } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CTimer + /// @brief Start the timer by given time in milliseconds to make his call + /// by arrive of them. + /// + /// If interval is activated, it calls the associated callback function + /// continuously in the given interval. + /// + /// @param[in] timeout Timeout in milliseconds + /// @param[in] interval [opt] To run continuously if true, false only one time + /// and default + /// @return True if successfully done, false if not (callback missing, + /// timeout = 0 or was already running. + /// + bool Start(uint64_t timeout, bool interval = false) + { + using namespace std::chrono; + + if (m_callback == nullptr || timeout == 0 || IsRunning()) + return false; + + m_timeout = milliseconds(timeout); + m_interval = interval; + + CreateThread(); + return true; + } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CTimer + /// @brief Stop the timer if it is active. + /// + /// @param[in] wait [opt] Wait until timer is stopped, false is default and + /// call unblocked + /// @return True if timer was active and was stopped, false if already was + /// stopped. + /// + bool Stop(bool wait = false) + { + if (!IsRunning()) + return false; + + m_threadStop = true; + m_eventTimeout.notify_all(); + StopThread(wait); + + return true; + } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CTimer + /// @brief Restart timer complete by stop and restart his thread again. + /// + /// @note Restart only possible as long the timer was running and not done his + /// work. + /// + /// @return True if start was successfully done, on error, or if was already + /// finished returned as false + /// + bool Restart() + { + using namespace std::chrono; + + if (!IsRunning()) + return false; + + Stop(true); + return Start(duration_cast<milliseconds>(m_timeout).count(), m_interval); + } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CTimer + /// @brief Restart the timer with new timeout without touch of his thread. + /// + /// @param[in] timeout Time as milliseconds to wait for next call + /// + void RestartAsync(uint64_t timeout) + { + using namespace std::chrono; + + m_timeout = milliseconds(timeout); + const auto now = system_clock::now(); + m_endTime = now.time_since_epoch() + m_timeout; + m_eventTimeout.notify_all(); + } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CTimer + /// @brief Check timer is still active to wait for next call. + /// + /// @return True if active, false if all his work done and no more running + /// + bool IsRunning() const { return CThread::IsRunning(); } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CTimer + /// @brief Get elapsed time as floating point of timer as seconds. + /// + /// @return Elapsed time + /// + float GetElapsedSeconds() const { return GetElapsedMilliseconds() / 1000.0f; } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @ingroup cpp_kodi_tools_CTimer + /// @brief Get elapsed time as floating point of timer as milliseconds. + /// + /// @return Elapsed time + /// + float GetElapsedMilliseconds() const + { + using namespace std::chrono; + + if (!IsRunning()) + return 0.0f; + + const auto now = system_clock::now(); + return duration_cast<milliseconds>(now.time_since_epoch() - (m_endTime - m_timeout)).count(); + } + //---------------------------------------------------------------------------- + + //============================================================================ + /// @defgroup cpp_kodi_tools_CTimer_CB class ITimerCallback + /// @ingroup cpp_kodi_tools_CTimer + /// @brief **Callback class of timer**\n + /// To give on contructor by @ref CTimer(kodi::tools::CTimer::ITimerCallback* callback) + /// + class ITimerCallback + { + public: + //========================================================================== + /// @ingroup cpp_kodi_tools_CTimer_CB + /// @brief Class destructor. + /// + virtual ~ITimerCallback() = default; + //-------------------------------------------------------------------------- + + //========================================================================== + /// @ingroup cpp_kodi_tools_CTimer_CB + /// @brief Callback function to implement if constuctor @ref CTimer(kodi::tools::CTimer::ITimerCallback* callback) + /// is used and this as parent on related class + /// + /// ---------------------------------------------------------------------------- + /// + /// **Example:** + /// ~~~~~~~~~~~~~{.cpp} + /// #include <kodi/tools/Timer.h> + /// + /// class CExample : public kodi::tools::CTimer, + /// private kodi::tools::CTimer::ITimerCallback + /// { + /// public: + /// CExample() : kodi::tools::CTimer(this) + /// { + /// } + /// + /// void OnTimeout() override + /// { + /// // Some work + /// } + /// }; + /// + /// ~~~~~~~~~~~~~ + /// + virtual void OnTimeout() = 0; + //-------------------------------------------------------------------------- + }; + //---------------------------------------------------------------------------- + +protected: + void Process() override + { + using namespace std::chrono; + + while (!m_threadStop) + { + auto currentTime = system_clock::now(); + m_endTime = currentTime.time_since_epoch() + m_timeout; + + // wait the necessary time + std::mutex mutex; + std::unique_lock<std::mutex> lock(mutex); + const auto waitTime = duration_cast<milliseconds>(m_endTime - currentTime.time_since_epoch()); + if (m_eventTimeout.wait_for(lock, waitTime) == std::cv_status::timeout) + { + currentTime = system_clock::now(); + if (m_endTime.count() <= currentTime.time_since_epoch().count()) + { + // execute OnTimeout() callback + m_callback(); + + // continue if this is an interval timer, or if it was restarted during callback + if (!m_interval && m_endTime.count() <= currentTime.time_since_epoch().count()) + break; + } + } + } + } + +private: + bool m_interval = false; + std::function<void()> m_callback; + std::chrono::system_clock::duration m_timeout; + std::chrono::system_clock::duration m_endTime; + std::condition_variable_any m_eventTimeout; +}; +///@} +//------------------------------------------------------------------------------ + +} /* namespace tools */ +} /* namespace kodi */ + +#endif /* __cplusplus */ diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/versions.h b/xbmc/addons/kodi-dev-kit/include/kodi/versions.h index 488425f42f..0908363884 100644 --- a/xbmc/addons/kodi-dev-kit/include/kodi/versions.h +++ b/xbmc/addons/kodi-dev-kit/include/kodi/versions.h @@ -73,7 +73,7 @@ #define ADDON_GLOBAL_VERSION_NETWORK_DEPENDS "Network.h" \ "c-api/network.h" -#define ADDON_GLOBAL_VERSION_TOOLS "1.0.1" +#define ADDON_GLOBAL_VERSION_TOOLS "1.0.2" #define ADDON_GLOBAL_VERSION_TOOLS_MIN "1.0.0" #define ADDON_GLOBAL_VERSION_TOOLS_XML_ID "kodi.binary.global.tools" #define ADDON_GLOBAL_VERSION_TOOLS_DEPENDS "tools/DllHelper.h" \ |