diff options
author | Nat Goodspeed <nat@lindenlab.com> | 2019-07-11 14:52:30 -0400 |
---|---|---|
committer | Nat Goodspeed <nat@lindenlab.com> | 2020-03-25 18:44:04 -0400 |
commit | d8e08ecde97362f6b3117cbb68395fdd299027e2 (patch) | |
tree | b3327c7897f50b0ce215c0e95a95329c3c28b6db | |
parent | f3baa6bbe512cca34f50ff84389491c3c8afc9df (diff) |
DRTVWR-476: WIP: Untested preliminary implementation of LLCond.
LLCond encapsulates the usage patterns required to properly use
condition_variable. We also provide LLScalarCond, LLBoolCond and LLOneShotCond.
-rw-r--r-- | indra/llcommon/llcond.h | 356 |
1 files changed, 356 insertions, 0 deletions
diff --git a/indra/llcommon/llcond.h b/indra/llcommon/llcond.h new file mode 100644 index 0000000000..adfeb27f82 --- /dev/null +++ b/indra/llcommon/llcond.h @@ -0,0 +1,356 @@ +/** + * @file llcond.h + * @author Nat Goodspeed + * @date 2019-07-10 + * @brief LLCond is a wrapper around condition_variable to encapsulate the + * obligatory condition_variable usage pattern. We also provide + * simplified versions LLScalarCond, LLBoolCond and LLOneShotCond. + * + * $LicenseInfo:firstyear=2019&license=viewerlgpl$ + * Copyright (c) 2019, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLCOND_H) +#define LL_LLCOND_H + +#include <boost/fiber/condition_variable.hpp> +#include <mutex> +#include <chrono> + +/** + * LLCond encapsulates the pattern required to use a condition_variable. It + * bundles subject data, a mutex and a condition_variable: the three required + * data objects. It provides wait() methods analogous to condition_variable, + * but using the contained condition_variable and the contained mutex. It + * provides modify() methods accepting an invocable to safely modify the + * contained data and notify waiters. These methods implicitly perform the + * required locking. + * + * The generic LLCond template assumes that DATA might be a struct or class. + * For a scalar DATA type, consider LLScalarCond instead. For specifically + * bool, consider LLBoolCond. + * + * Use of boost::fibers::condition_variable makes LLCond work between + * coroutines as well as between threads. + */ +template <typename DATA> +class LLCond +{ +private: + // This is the DATA controlled by the condition_variable. + DATA mData; + // condition_variable must be used in conjunction with a mutex. Use + // boost::fibers::mutex instead of std::mutex because the latter blocks + // the entire calling thread, whereas the former blocks only the current + // coroutine within the calling thread. Yet boost::fiber::mutex is safe to + // use across threads as well: it subsumes std::mutex functionality. + boost::fibers::mutex mMutex; + // Use boost::fibers::condition_variable for the same reason. + boost::fibers::condition_variable mCond; + +public: + /// LLCond can be explicitly initialized with a specific value for mData if + /// desired. + LLCond(DATA&& init=DATA()): + mData(init) + {} + + /// LLCond is move-only + LLCond(const LLCond&) = delete; + LLCond& operator=(const LLCond&) = delete; + + /// get() returns a const reference to the stored DATA. The only way to + /// get a non-const reference -- to modify the stored DATA -- is via + /// update_one() or update_all(). + const DATA& get() const { return mData; } + + /** + * Pass update_one() an invocable accepting non-const (DATA&). The + * invocable will presumably modify the referenced DATA. update_one() + * will lock the mutex, call the invocable and then call notify_one() on + * the condition_variable. + * + * For scalar DATA, it's simpler to use LLScalarCond::set_one(). Use + * update_one() when DATA is a struct or class. + */ + template <typename MODIFY> + void update_one(MODIFY modify) + { + { // scope of lock can/should end before notify_one() + std::unique_lock<boost::fibers::mutex> lk(mMutex); + modify(mData); + } + mCond.notify_one(); + } + + /** + * Pass update_all() an invocable accepting non-const (DATA&). The + * invocable will presumably modify the referenced DATA. update_all() + * will lock the mutex, call the invocable and then call notify_all() on + * the condition_variable. + * + * For scalar DATA, it's simpler to use LLScalarCond::set_all(). Use + * update_all() when DATA is a struct or class. + */ + template <typename MODIFY> + void update_all(MODIFY modify) + { + { // scope of lock can/should end before notify_all() + std::unique_lock<boost::fibers::mutex> lk(mMutex); + modify(mData); + } + mCond.notify_all(); + } + + /** + * Pass wait() a predicate accepting (const DATA&), returning bool. The + * predicate returns true when the condition for which it is waiting has + * been satisfied, presumably determined by examining the referenced DATA. + * wait() locks the mutex and, until the predicate returns true, calls + * wait() on the condition_variable. + */ + template <typename Pred> + void wait(Pred pred) + { + std::unique_lock<boost::fibers::mutex> lk(mMutex); + // We must iterate explicitly since the predicate accepted by + // condition_variable::wait() requires a different signature: + // condition_variable::wait() calls its predicate with no arguments. + // Fortunately, the loop is straightforward. + // We advise the caller to pass a predicate accepting (const DATA&). + // But what if they instead pass a predicate accepting non-const + // (DATA&)? Such a predicate could modify mData, which would be Bad. + // Forbid that. + while (! pred(const_cast<const DATA&>(mData))) + { + mCond.wait(lk); + } + } + + /** + * Pass wait_until() a chrono::time_point, indicating the time at which we + * should stop waiting, and a predicate accepting (const DATA&), returning + * bool. The predicate returns true when the condition for which it is + * waiting has been satisfied, presumably determined by examining the + * referenced DATA. wait_until() locks the mutex and, until the predicate + * returns true, calls wait_until() on the condition_variable. + * wait_until() returns false if condition_variable::wait_until() timed + * out without the predicate returning true. + */ + template <typename Clock, typename Duration, typename Pred> + bool wait_until(const std::chrono::time_point<Clock, Duration>& timeout_time, Pred pred) + { + std::unique_lock<boost::fibers::mutex> lk(mMutex); + // see wait() for comments about this const_cast + while (! pred(const_cast<const DATA&>(mData))) + { + if (boost::fibers::cv_status::timeout == mCond.wait_until(lk, timeout_time)) + { + // It's possible that wait_until() timed out AND the predicate + // became true more or less simultaneously. Even though + // wait_until() timed out, check the predicate one more time. + return pred(const_cast<const DATA&>(mData)); + } + } + return true; + } + + /** + * Pass wait_for() a chrono::duration, indicating how long we're willing + * to wait, and a predicate accepting (const DATA&), returning bool. The + * predicate returns true when the condition for which it is waiting has + * been satisfied, presumably determined by examining the referenced DATA. + * wait_for() locks the mutex and, until the predicate returns true, calls + * wait_for() on the condition_variable. wait_for() returns false if + * condition_variable::wait_for() timed out without the predicate + * returning true. + */ + template <typename Rep, typename Period, typename Pred> + bool wait_for(const std::chrono::duration<Rep, Period>& timeout_duration, Pred pred) + { + // Instead of replicating wait_until() logic, convert duration to + // time_point and just call wait_until(). + // An implementation in which we repeatedly called + // condition_variable::wait_for() with our passed duration would be + // wrong! We'd keep pushing the timeout time farther and farther into + // the future. This way, we establish a definite timeout time and + // stick to it. + return wait_until(std::chrono::steady_clock::now() + timeout_duration, pred); + } +}; + +template <typename DATA> +class LLScalarCond: public LLCond<DATA> +{ + using super = LLCond<DATA>; + +public: + /// LLScalarCond can be explicitly initialized with a specific value for + /// mData if desired. + LLCond(DATA&& init=DATA()): + super(init) + {} + + /// Pass set_one() a new value to which to update mData. set_one() will + /// lock the mutex, update mData and then call notify_one() on the + /// condition_variable. + void set_one(DATA&& value) + { + super::update_one([](DATA& data){ data = value; }); + } + + /// Pass set_all() a new value to which to update mData. set_all() will + /// lock the mutex, update mData and then call notify_all() on the + /// condition_variable. + void set_all(DATA&& value) + { + super::update_all([](DATA& data){ data = value; }); + } + + /** + * Pass wait_equal() a value for which to wait. wait_equal() locks the + * mutex and, until the stored DATA equals that value, calls wait() on the + * condition_variable. + */ + void wait_equal(const DATA& value) + { + super::wait([&value](const DATA& data){ return (data == value); }); + } + + /** + * Pass wait_until_equal() a chrono::time_point, indicating the time at + * which we should stop waiting, and a value for which to wait. + * wait_until_equal() locks the mutex and, until the stored DATA equals + * that value, calls wait_until() on the condition_variable. + * wait_until_equal() returns false if condition_variable::wait_until() + * timed out without the stored DATA being equal to the passed value. + */ + template <typename Clock, typename Duration> + bool wait_until_equal(const std::chrono::time_point<Clock, Duration>& timeout_time, + const DATA& value) + { + return super::wait_until(timeout_time, + [&value](const DATA& data){ return (data == value); }); + } + + /** + * Pass wait_for_equal() a chrono::duration, indicating how long we're + * willing to wait, and a value for which to wait. wait_for_equal() locks + * the mutex and, until the stored DATA equals that value, calls + * wait_for() on the condition_variable. wait_for_equal() returns false if + * condition_variable::wait_for() timed out without the stored DATA being + * equal to the passed value. + */ + template <typename Rep, typename Period> + bool wait_for_equal(const std::chrono::duration<Rep, Period>& timeout_duration, + const DATA& value) + { + return super::wait_for(timeout_duration, + [&value](const DATA& data){ return (data == value); }); + } + + /** + * Pass wait_unequal() a value from which to move away. wait_unequal() + * locks the mutex and, until the stored DATA no longer equals that value, + * calls wait() on the condition_variable. + */ + void wait_unequal(const DATA& value) + { + super::wait([&value](const DATA& data){ return (data != value); }); + } + + /** + * Pass wait_until_unequal() a chrono::time_point, indicating the time at + * which we should stop waiting, and a value from which to move away. + * wait_until_unequal() locks the mutex and, until the stored DATA no + * longer equals that value, calls wait_until() on the condition_variable. + * wait_until_unequal() returns false if condition_variable::wait_until() + * timed out with the stored DATA still being equal to the passed value. + */ + template <typename Clock, typename Duration> + bool wait_until_unequal(const std::chrono::time_point<Clock, Duration>& timeout_time, + const DATA& value) + { + return super::wait_until(timeout_time, + [&value](const DATA& data){ return (data != value); }); + } + + /** + * Pass wait_for_unequal() a chrono::duration, indicating how long we're + * willing to wait, and a value from which to move away. + * wait_for_unequal() locks the mutex and, until the stored DATA no longer + * equals that value, calls wait_for() on the condition_variable. + * wait_for_unequal() returns false if condition_variable::wait_for() + * timed out with the stored DATA still being equal to the passed value. + */ + template <typename Rep, typename Period> + bool wait_for_unequal(const std::chrono::duration<Rep, Period>& timeout_duration, + const DATA& value) + { + return super::wait_for(timeout_duration, + [&value](const DATA& data){ return (data != value); }); + } +}; + +/// Using bool as LLScalarCond's DATA seems like a particularly useful case +using LLBoolCond = LLScalarCond<bool>; + +// LLOneShotCond -- init false, set (and wait for) true? Or full suite? +class LLOneShotCond: public LLBoolCond +{ + using super = LLBoolCond; + +public: + /// The bool stored in LLOneShotCond is initially false + LLOneShotCond(): super(false) {} + + /// LLOneShotCond assumes that nullary set_one() means to set its bool true + void set_one(bool value=true) + { + super::set_one(value); + } + + /// LLOneShotCond assumes that nullary set_all() means to set its bool true + void set_all(bool value=true) + { + super::set_all(value); + } + + /** + * wait() locks the mutex and, until the stored bool is true, calls wait() + * on the condition_variable. + */ + void wait() + { + super::wait_equal(true); + } + + /** + * Pass wait_until() a chrono::time_point, indicating the time at which we + * should stop waiting. wait_until() locks the mutex and, until the stored + * bool is true, calls wait_until() on the condition_variable. + * wait_until() returns false if condition_variable::wait_until() timed + * out without the stored bool being true. + */ + template <typename Clock, typename Duration> + bool wait_until(const std::chrono::time_point<Clock, Duration>& timeout_time) + { + return super::wait_until_equal(timeout_time, true); + } + + /** + * Pass wait_for() a chrono::duration, indicating how long we're willing + * to wait. wait_for() locks the mutex and, until the stored bool is true, + * calls wait_for() on the condition_variable. wait_for() returns false if + * condition_variable::wait_for() timed out without the stored bool being + * true. + */ + template <typename Rep, typename Period> + bool wait_for(const std::chrono::duration<Rep, Period>& timeout_duration) + { + return super::wait_for_equal(timeout_duration, true); + } +}; + +#endif /* ! defined(LL_LLCOND_H) */ |