/** * @file sync.h * @author Nat Goodspeed * @date 2019-03-13 * @brief Synchronize coroutines within a test program so we can observe side * effects. Certain test programs test coroutine synchronization * mechanisms. Such tests usually want to interleave coroutine * executions in strictly stepwise fashion. This class supports that * paradigm. * * $LicenseInfo:firstyear=2019&license=viewerlgpl$ * Copyright (c) 2019, Linden Research, Inc. * $/LicenseInfo$ */ #if ! defined(LL_SYNC_H) #define LL_SYNC_H #include "llcond.h" #include "lltut.h" #include "stringize.h" #include "llerror.h" #include "llcoros.h" /** * Instantiate Sync in any test in which we need to suspend one coroutine * until we're sure that another has had a chance to run. Simply calling * llcoro::suspend() isn't necessarily enough; that provides a chance for the * other to run, but doesn't guarantee that it has. If each coroutine is * consistent about calling Sync::bump() every time it wakes from any * suspension, Sync::yield() and yield_until() should at least ensure that * somebody else has had a chance to run. */ class Sync { LLScalarCond mCond{0}; F32Milliseconds mTimeout; public: Sync(F32Milliseconds timeout=F32Milliseconds(10000.0f)): mTimeout(timeout) {} /** * Bump mCond by n steps -- ideally, do this every time a participating * coroutine wakes up from any suspension. The choice to bump() after * resumption rather than just before suspending is worth calling out: * this practice relies on the fact that condition_variable::notify_all() * merely marks a suspended coroutine ready to run, rather than * immediately resuming it. This way, though, even if a coroutine exits * before reaching its next suspend point, the other coroutine isn't * left waiting forever. */ void bump(int n=1) { // Calling mCond.set_all(mCond.get() + n) would be great for // coroutines -- but not so good between kernel threads -- it would be // racy. Make the increment atomic by calling update_all(), which runs // the passed lambda within a mutex lock. int updated; mCond.update_all( [&n, &updated](int& data) { data += n; // Capture the new value for possible logging purposes. updated = data; }); // In the multi-threaded case, this log message could be a bit // misleading, as it will be emitted after waiting threads have // already awakened. But emitting the log message within the lock // would seem to hold the lock longer than we really ought. LL_DEBUGS() << LLCoros::getName() << " bump(" << n << ") -> " << updated << LL_ENDL; } /** * Set mCond to a specific n. Use of bump() and yield() is nicely * maintainable, since you can insert or delete matching operations in a * test function and have the rest of the Sync operations continue to * line up as before. But sometimes you need to get very specific, which * is where set() and yield_until() come in handy: less maintainable, * more precise. */ void set(int n) { LL_DEBUGS() << LLCoros::getName() << " set(" << n << ")" << LL_ENDL; mCond.set_all(n); } /// suspend until "somebody else" has bumped mCond by n steps void yield(int n=1) { return yield_until("Sync::yield_for", n, mCond.get() + n); } /// suspend until "somebody else" has bumped mCond to a specific value void yield_until(int until) { return yield_until("Sync::yield_until", until, until); } private: void yield_until(const char* func, int arg, int until) { std::string name(LLCoros::getName()); LL_DEBUGS() << name << " yield_until(" << until << ") suspending" << LL_ENDL; if (! mCond.wait_for_equal(mTimeout, until)) { tut::fail(STRINGIZE(name << ' ' << func << '(' << arg << ") timed out after " << int(mTimeout.value()) << "ms (expected " << until << ", actual " << mCond.get() << ')')); } // each time we wake up, bump mCond bump(); } }; #endif /* ! defined(LL_SYNC_H) */