diff options
Diffstat (limited to 'src/async')
-rw-r--r-- | src/async/CMakeLists.txt | 13 | ||||
-rw-r--r-- | src/async/async.cpp | 67 | ||||
-rw-r--r-- | src/async/async.h | 37 | ||||
-rw-r--r-- | src/async/background-progress.h | 41 | ||||
-rw-r--r-- | src/async/channel.h | 184 | ||||
-rw-r--r-- | src/async/progress-splitter.h | 73 | ||||
-rw-r--r-- | src/async/progress.h | 155 |
7 files changed, 570 insertions, 0 deletions
diff --git a/src/async/CMakeLists.txt b/src/async/CMakeLists.txt new file mode 100644 index 0000000..c6bafb6 --- /dev/null +++ b/src/async/CMakeLists.txt @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(async_SRC + async.cpp + + async.h + channel.h + background-progress.h + progress.h + progress-splitter.h +) + +add_inkscape_source("${async_SRC}") diff --git a/src/async/async.cpp b/src/async/async.cpp new file mode 100644 index 0000000..7bcebbb --- /dev/null +++ b/src/async/async.cpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include <vector> +#include <algorithm> +#include <mutex> +#include <chrono> +#include "async.h" +#include "util/statics.h" + +namespace { + +// Todo: Replace when C++ gets an .is_ready(). +bool is_ready(std::future<void> const &future) +{ + return future.wait_for(std::chrono::seconds(0)) == std::future_status::ready; +} + +// Holds on to asyncs and waits for them to finish at program exit. +class AsyncBin +{ +public: + static auto const &get() + { + /* + * Using Static<AsyncBin> to ensure destruction before main() exits, so that lifetimes + * of background threads are synchronized with the destruction of statics. + */ + static Inkscape::Util::Static<AsyncBin const> instance; + return instance.get(); + } + + void add(std::future<void> &&future) const + { + auto g = std::lock_guard(mutables); + futures.erase( + std::remove_if(futures.begin(), futures.end(), [] (auto const &future) { + return is_ready(future); + }), + futures.end()); + futures.emplace_back(std::move(future)); + } + + ~AsyncBin() { drain(); } + +private: + mutable std::mutex mutables; + mutable std::vector<std::future<void>> futures; + + auto grab() const + { + auto g = std::lock_guard(mutables); + return std::move(futures); + } + + void drain() const { while (!grab().empty()) {} } +}; + +} // namespace + +namespace Inkscape { +namespace Async { +namespace detail { + +void extend(std::future<void> &&future) { AsyncBin::get().add(std::move(future)); } + +} // namespace detail +} // namespace Async +} // namespace Inkscape diff --git a/src/async/async.h b/src/async/async.h new file mode 100644 index 0000000..f4e3003 --- /dev/null +++ b/src/async/async.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file Async + * Fire-and-forget asyncs without UB at program exit. + * + * This file provides asyncs whose futures do not block on destruction, while + * ensuring program exit is delayed until all such asyncs have terminated, in + * order to ensure clean termination of asyncs and avoid undefined behaivour. + * + * Related: https://open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3451.pdf + */ +#ifndef INKSCAPE_ASYNC_H +#define INKSCAPE_ASYNC_H + +#include <future> +#include <utility> + +namespace Inkscape { +namespace Async { +namespace detail { + +void extend(std::future<void> &&future); + +} // namespace detail + +/** + * Launch an async which will delay program exit until its termination. + */ +template <typename F> +inline void fire_and_forget(F &&f) +{ + detail::extend(std::async(std::launch::async, std::forward<F>(f))); +} + +} // namespace Async +} // namespace Inkscape + +#endif // INKSCAPE_ASYNC_H diff --git a/src/async/background-progress.h b/src/async/background-progress.h new file mode 100644 index 0000000..49fe3d7 --- /dev/null +++ b/src/async/background-progress.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file Background-progress + * A Progress object that reports progress thread-safely over a Channel. + */ +#ifndef INKSCAPE_ASYNC_BACKGROUND_PROGRESS_H +#define INKSCAPE_ASYNC_BACKGROUND_PROGRESS_H + +#include <functional> +#include "channel.h" +#include "progress.h" + +namespace Inkscape { +namespace Async { + +template <typename... T> +class BackgroundProgress final + : public Progress<T...> +{ +public: + /** + * Construct a Progress object which becomes cancelled as soon as \a channel is closed, + * and reports progress by calling \a onprogress over \a channel. + * + * The result can only be used within the lifetime of \a channel. + */ + BackgroundProgress(Channel::Source &channel, std::function<void(T...)> &onprogress) + : channel(&channel) + , onprogress(std::move(onprogress)) {} + +private: + Channel::Source *channel; + std::function<void(T...)> onprogress; + + bool _keepgoing() const override { return channel->operator bool(); } + bool _report(T const &... progress) override { return channel->run(std::bind(onprogress, progress...)); } +}; + +} // namespace Async +} // namespace Inkscape + +#endif // INKSCAPE_ASYNC_BACKGROUND_PROGRESS_H diff --git a/src/async/channel.h b/src/async/channel.h new file mode 100644 index 0000000..d58841e --- /dev/null +++ b/src/async/channel.h @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file Channel + * Thread-safe communication channel for asyncs. + */ +#ifndef INKSCAPE_ASYNC_CHANNEL_H +#define INKSCAPE_ASYNC_CHANNEL_H + +#include <memory> +#include <optional> +#include <mutex> +#include <utility> +#include <glibmm/dispatcher.h> +#include "util/funclog.h" + +namespace Inkscape { +namespace Async { +namespace Channel { +namespace detail { + +class Shared final + : public std::enable_shared_from_this<Shared> +{ +public: + Shared() + { + dispatcher->connect([this] { + auto ref = shared_from_this(); + grab().exec_while([this] { return is_open; }); + }); + } + + Shared(Shared const &) = delete; + Shared &operator=(Shared const &) = delete; + + operator bool() const + { + auto g = std::lock_guard(mutables); + return is_open; + } + + template <typename F> + bool run(F &&f) const + { + auto g = std::lock_guard(mutables); + if (!is_open) return false; + if (funclog.empty()) dispatcher->emit(); + funclog.emplace(std::forward<F>(f)); + return true; + } + + void close() + { + disconnect_source(); + dispatcher.reset(); + funclog.clear(); + } + +private: + mutable std::mutex mutables; + mutable std::optional<Glib::Dispatcher> dispatcher = std::make_optional<Glib::Dispatcher>(); + mutable Util::FuncLog funclog; + bool is_open = true; + + Util::FuncLog grab() const + { + auto g = std::lock_guard(mutables); + return std::move(funclog); + } + + void disconnect_source() + { + auto g = std::lock_guard(mutables); + is_open = false; + } +}; + +struct Create; + +} // namespace detail + +class Source final +{ +public: + Source() = default; + Source(Source const &) = delete; + Source &operator=(Source const &) = delete; + Source(Source &&) = default; + Source &operator=(Source &&) = default; + + /** + * Check whether the channel is still open. + */ + explicit operator bool() const { return shared && shared->operator bool(); } + + /** + * Attempt to run a function on the main loop that the Channel was created in. This will + * either succeed and execute or destroy the function in the main loop's thread, or fail and + * leave it untouched. + * + * \return Whether the Channel is still open at the time of calling. + * + * Note that a return value of true doesn't indicate whether the function will actually run, + * because the Channel could be closed in the meantime. If it does run, it is guaranteed the + * Dest object still exists and \a close() has not been called on it. + */ + template <typename F> + bool run(F &&f) const { return shared && shared->run(std::forward<F>(f)); } + + /** + * Close the channel. No more functions submitted through run() will be run. + */ + void close() { shared.reset(); } + +private: + std::shared_ptr<detail::Shared const> shared; + explicit Source(std::shared_ptr<detail::Shared> shared_) : shared(std::move(shared_)) {} + friend struct detail::Create; +}; + +class Dest final +{ +public: + Dest() = default; + Dest(Dest const &) = delete; + Dest &operator=(Dest const &) = delete; + Dest(Dest &&) = default; + Dest &operator=(Dest &&) = default; + ~Dest() { close(); } + + /** + * Close the channel. No further functions submitted by the other end will be run, and it will + * be notified of closure whenever it checks. + */ + void close() { if (shared) { shared->close(); shared.reset(); } } + + /** + * Check whether \a close() has already been called, or if the channel was never opened. + * + * Note: This does not check whether the corresponding \a close() method of Source has been + * called. In fact, this condition is meaningless without further synchronization. If you need + * to know whether the Source has closed, you can have it manually send this information + * over the Channel instead. + */ + explicit operator bool() const { return (bool)shared; } + +private: + std::shared_ptr<detail::Shared> shared; + explicit Dest(std::shared_ptr<detail::Shared> shared_) : shared(std::move(shared_)) {} + friend struct detail::Create; +}; + +namespace detail { + +struct Create +{ + Create() = delete; + + static auto create() + { + auto shared = std::make_shared<detail::Shared>(); + auto src = Source(shared); + auto dst = Dest(std::move(shared)); + return std::make_pair(std::move(src), std::move(dst)); + } +}; + +} // namespace detail + +/** + * Create a linked Source - Destination pair forming a thread-safe communication channel. + * + * As long as the channel is still open, the Source can use it to run commands in the main loop of + * the creation thread and check if the channel is still open. Destructing either end closes the channel. + */ +inline std::pair<Source, Dest> create() +{ + return detail::Create::create(); +} + +} // namespace Channel +} // namespace Async +} // namespace Inkscape + +#endif // INKSCAPE_ASYNC_CHANNEL_H diff --git a/src/async/progress-splitter.h b/src/async/progress-splitter.h new file mode 100644 index 0000000..9666611 --- /dev/null +++ b/src/async/progress-splitter.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file Progress-splitter + * Dynamically split a Progress into several sub-tasks. + */ +#ifndef INKSCAPE_ASYNC_PROGRESS_SPLITTER_H +#define INKSCAPE_ASYNC_PROGRESS_SPLITTER_H + +#include <vector> +#include <optional> +#include "progress.h" + +namespace Inkscape { +namespace Async { + +/** + * A RAII object for splitting a Progress into a dynamically-determined collection of sub-tasks. + */ +template <typename T, typename... S> +class ProgressSplitter +{ +public: + /// Construct a progress splitter for a given task. + ProgressSplitter(Progress<T, S...> &parent) : parent(&parent) {} + + /// Add a SubProgress which makes progress \a amount. + ProgressSplitter &add(std::optional<SubProgress<T, S...>> &progress, T amount) + { + entries.push_back({ &progress, amount }); + return *this; + } + + /// Convenience method to enable a "fluent interface". Calls \a add if \a condition is true. + ProgressSplitter &add_if(std::optional<SubProgress<T, S...>> &progress, T amount, bool condition) + { + return condition ? add(progress, amount) : *this; + } + + /// Assign to each added SubProgress its portion of the total progress. + ~ProgressSplitter() { apportion(); } + +private: + struct Entry + { + std::optional<SubProgress<T, S...>> *progress; + T amount; + }; + + Progress<T, S...> *parent; + std::vector<Entry> entries; + + void apportion() noexcept + { + if (entries.empty()) { + return; + } + + T total = 0; + for (auto const &e : entries) { + total += e.amount; + } + + T from = 0; + for (auto &e : entries) { + e.progress->emplace(*parent, from / total, e.amount / total); + from += e.amount; + } + } +}; + +} // namespace Async +} // namespace Inkscape + +#endif // INKSCAPE_ASYNC_PROGRESS_SPLITTER_H diff --git a/src/async/progress.h b/src/async/progress.h new file mode 100644 index 0000000..120f03b --- /dev/null +++ b/src/async/progress.h @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file Progress + * Interface for reporting progress and checking cancellation. + */ +#ifndef INKSCAPE_ASYNC_PROGRESS_H +#define INKSCAPE_ASYNC_PROGRESS_H + +#include <chrono> + +namespace Inkscape { +namespace Async { + +class CancelledException {}; + +/** + * An interface for tasks to report progress and check for cancellation. + * Not supported: + * - Error reporting - use exceptions! + * - Thread-safety - overrides should provide this if needed using e.g. BackgroundProgress. + */ +template <typename... T> +class Progress +{ +public: + /// Report a progress value, returning false if cancelled. + bool report(T const &... progress) { return _report(progress...); } + + /// Report a progress value, throwing CancelledException if cancelled. + void report_or_throw(T const &... progress) { if (!_report(progress...)) throw CancelledException(); } + + /// Return whether not cancelled. + bool keepgoing() const { return _keepgoing(); } + + /// Throw CancelledException if cancelled. + void throw_if_cancelled() const { if (!_keepgoing()) throw CancelledException(); } + + /// Convenience function - same as check(). + operator bool() const { return _keepgoing(); } + +protected: + ~Progress() = default; + virtual bool _keepgoing() const = 0; + virtual bool _report(T const &... progress) = 0; +}; + +/** + * A Progress object representing a sub-task of another Progress. + */ +template <typename T, typename... S> +class SubProgress final + : public Progress<T, S...> +{ +public: + /// Construct a progress object for a sub-task. + SubProgress(Progress<T, S...> &parent, T from, T amount) + { + if (auto p = dynamic_cast<SubProgress*>(&parent)) { + _root = p->_root; + _from = p->_from + p->_amount * from; + _amount = p->_amount * amount; + } else { + _root = &parent; + _from = from; + _amount = amount; + } + } + +private: + Progress<T, S...> *_root; + T _from, _amount; + + bool _keepgoing() const override { return _root->keepgoing(); } + bool _report(T const &progress, S const &... aux) override { return _root->report(_from + _amount * progress, aux...); } +}; + +/** + * A Progress object that throttles reports to a given step size. + */ +template <typename T, typename... S> +class ProgressStepThrottler final + : public Progress<T, S...> +{ +public: + ProgressStepThrottler(Progress<T, S...> &parent, T step) + : parent(&parent), step(step) {} + +private: + Progress<T, S...> *parent; + T step; + T last = 0; + + bool _keepgoing() const override { return parent->keepgoing(); } + + bool _report(T const &progress, S const &... aux) override + { + if (progress - last < step) { + return parent->keepgoing(); + } else { + last = progress; + return parent->report(progress, aux...); + } + } +}; + +/** + * A Progress object that throttles reports to a given time interval. + */ +template <typename... T> +class ProgressTimeThrottler final + : public Progress<T...> +{ + using clock = std::chrono::steady_clock; + using time_point = clock::time_point; + +public: + using duration = clock::duration; + + ProgressTimeThrottler(Progress<T...> &parent, duration interval) + : parent(&parent), interval(interval) {} + +private: + Progress<T...> *parent; + duration interval; + time_point last = clock::now(); + + bool _keepgoing() const override { return parent->keepgoing(); } + + bool _report(T const &... progress) override + { + auto now = clock::now(); + if (now - last < interval) { + return parent->keepgoing(); + } else { + last = now; + return parent->report(progress...); + } + } +}; + +/** + * A dummy Progress object that never reports cancellation. + */ +template <typename... T> +class ProgressAlways final + : public Progress<T...> +{ +private: + bool _keepgoing() const override { return true; } + bool _report(T const &...) override { return true; } +}; + +} // namespace Async +} // namespace Inkscape + +#endif // INKSCAPE_ASYNC_PROGRESS_H |