From a625f2246ef33143de27c1502ec166d8dffbc116 Mon Sep 17 00:00:00 2001 From: Steve O'Brien Date: Thu, 17 Sep 2015 06:30:23 -0700 Subject: [PATCH] folly Singleton: "eager" option to initialize upfront Summary: Instead of the default lazy-loading behavior (still the default) some singletons might need to get initialized at startup time. This would be for singletons which take a long time for the instance's constructor to run, e.g. expensive initialization by reading some large dataset, talking to an outside service, and so on. Provides a way for singletons to opt-in to this, and get populated at the time `registrationComplete()` is called, instead of lazily. Some notes about the way I implemented here, mainly, why I did this as a "builder-pattern" kind of thing and not some other way. I could probably be convinced to do otherwise. :) * Changing the constructor: the constructor's already slightly fiddly with the two optional -- well, one optional construct function, and another optional-but-only-if-construct-provided, destruct function. Didn't want to pile more into the ctor. * New superclass called `EagerLoadedSingleton`; just didn't want to add more classes, esp. if it's just to add one more option. * Method like `void setEagerLoad()` that makes this eager-load; not sure where one would write the `shouldEagerLoad()` call, probably in some central initialization spot in `main()`, but all the maintenance would have to go there. I like that it's "attached" to the singleton being defined. (Though you can still do this.) Bonus #2; the rule that builds the cpp containing "main" doesn't need to import this dependency and the cpp doesn't have to include Singleton just to do this eager-load call, nor the header for the type itself. * Omitting this altogether and just saying `folly::Singleton::get_weak()` to "ping" the singleton and bring into existence: see last point. Still might need to have the file containing this initialization decorum include/link against Foo, as well as have one place to maintain the list of things to load up-front. Reviewed By: @meyering Differential Revision: D2449081 --- folly/Singleton-inl.h | 35 +++++--- folly/Singleton.h | 155 +++++++++++++++++++++++++++++++---- folly/test/SingletonTest.cpp | 125 ++++++++++++++++++++++++++++ 3 files changed, 289 insertions(+), 26 deletions(-) diff --git a/folly/Singleton-inl.h b/folly/Singleton-inl.h index d3e6ea3024e..e946e8a51ba 100644 --- a/folly/Singleton-inl.h +++ b/folly/Singleton-inl.h @@ -135,20 +135,35 @@ SingletonHolder::SingletonHolder(TypeDescriptor type__, type_(type__), vault_(vault) { } +template +bool SingletonHolder::creationStarted() { + // If alive, then creation was of course started. + // This is flipped after creating_thread_ was set, and before it was reset. + if (state_.load(std::memory_order_acquire) == SingletonHolderState::Living) { + return true; + } + + // Not yet built. Is it currently in progress? + if (creating_thread_.load(std::memory_order_acquire) != std::thread::id()) { + return true; + } + + return false; +} + template void SingletonHolder::createInstance() { - // There's no synchronization here, so we may not see the current value - // for creating_thread if it was set by other thread, but we only care about - // it if it was set by current thread anyways. - if (creating_thread_ == std::this_thread::get_id()) { + if (creating_thread_.load(std::memory_order_acquire) == + std::this_thread::get_id()) { LOG(FATAL) << "circular singleton dependency: " << type_.name(); } std::lock_guard entry_lock(mutex_); - if (state_ == SingletonHolderState::Living) { + if (state_.load(std::memory_order_acquire) == SingletonHolderState::Living) { return; } - if (state_ == SingletonHolderState::NotRegistered) { + if (state_.load(std::memory_order_acquire) == + SingletonHolderState::NotRegistered) { auto ptr = SingletonVault::stackTraceGetter().load(); LOG(FATAL) << "Creating instance for unregistered singleton: " << type_.name() << "\n" @@ -156,7 +171,7 @@ void SingletonHolder::createInstance() { << "\n" << (ptr ? (*ptr)() : "(not available)"); } - if (state_ == SingletonHolderState::Living) { + if (state_.load(std::memory_order_acquire) == SingletonHolderState::Living) { return; } @@ -164,10 +179,10 @@ void SingletonHolder::createInstance() { // Clean up creator thread when complete, and also, in case of errors here, // so that subsequent attempts don't think this is still in the process of // being built. - creating_thread_ = std::thread::id(); + creating_thread_.store(std::thread::id(), std::memory_order_release); }; - creating_thread_ = std::this_thread::get_id(); + creating_thread_.store(std::this_thread::get_id(), std::memory_order_release); RWSpinLock::ReadHolder rh(&vault_.stateMutex_); if (vault_.state_ == SingletonVault::SingletonVaultState::Quiescing) { @@ -216,7 +231,7 @@ void SingletonHolder::createInstance() { // This has to be the last step, because once state is Living other threads // may access instance and instance_weak w/o synchronization. - state_.store(SingletonHolderState::Living); + state_.store(SingletonHolderState::Living, std::memory_order_release); { RWSpinLock::WriteHolder wh(&vault_.mutex_); diff --git a/folly/Singleton.h b/folly/Singleton.h index 0a1caadfb54..65320c865b4 100644 --- a/folly/Singleton.h +++ b/folly/Singleton.h @@ -71,6 +71,26 @@ // Where create and destroy are functions, Singleton::CreateFunc // Singleton::TeardownFunc. // +// The above examples detail a situation where an expensive singleton is loaded +// on-demand (thus only if needed). However if there is an expensive singleton +// that will likely be needed, and initialization takes a potentially long time, +// e.g. while initializing, parsing some files, talking to remote services, +// making uses of other singletons, and so on, the initialization of those can +// be scheduled up front, or "eagerly". +// +// In that case the singleton can be declared this way: +// +// namespace { +// auto the_singleton = +// folly::Singleton(/* optional create, destroy args */) +// .shouldEagerInit(); +// } +// +// This way the singleton's instance is built at program initialization +// time, or more accurately, when "registrationComplete()" or +// "startEagerInit()" is called. (More about that below; see the +// section starting with "A vault goes through a few stages of life".) +// // What if you need to destroy all of your singletons? Say, some of // your singletons manage threads, but you need to fork? Or your unit // test wants to clean up all global state? Then you can call @@ -89,6 +109,7 @@ #include #include #include +#include #include #include @@ -98,6 +119,7 @@ #include #include #include +#include #include #include #include @@ -197,6 +219,8 @@ class SingletonHolderBase { virtual TypeDescriptor type() = 0; virtual bool hasLiveInstance() = 0; + virtual void createInstance() = 0; + virtual bool creationStarted() = 0; virtual void destroyInstance() = 0; protected: @@ -224,15 +248,15 @@ struct SingletonHolder : public SingletonHolderBase { void registerSingleton(CreateFunc c, TeardownFunc t); void registerSingletonMock(CreateFunc c, TeardownFunc t); - virtual TypeDescriptor type(); - virtual bool hasLiveInstance(); - virtual void destroyInstance(); + virtual TypeDescriptor type() override; + virtual bool hasLiveInstance() override; + virtual void createInstance() override; + virtual bool creationStarted() override; + virtual void destroyInstance() override; private: SingletonHolder(TypeDescriptor type, SingletonVault& vault); - void createInstance(); - enum class SingletonHolderState { NotRegistered, Dead, @@ -250,7 +274,7 @@ struct SingletonHolder : public SingletonHolderBase { std::atomic state_{SingletonHolderState::NotRegistered}; // the thread creating the singleton (only valid while creating an object) - std::thread::id creating_thread_; + std::atomic creating_thread_; // The singleton itself and related functions. @@ -312,26 +336,99 @@ class SingletonVault { singletons_[entry->type()] = entry; } + /** + * Called by `Singleton.shouldEagerInit()` to ensure the instance + * is built when registrationComplete() is called; see that method + * for more info. + */ + void addEagerInitSingleton(detail::SingletonHolderBase* entry) { + RWSpinLock::ReadHolder rh(&stateMutex_); + + stateCheck(SingletonVaultState::Running); + + if (UNLIKELY(registrationComplete_)) { + throw std::logic_error( + "Registering for eager-load after registrationComplete()."); + } + + RWSpinLock::ReadHolder rhMutex(&mutex_); + CHECK_THROW(singletons_.find(entry->type()) != singletons_.end(), + std::logic_error); + + RWSpinLock::UpgradedHolder wh(&mutex_); + eagerInitSingletons_.insert(entry); + } + // Mark registration is complete; no more singletons can be - // registered at this point. - void registrationComplete() { + // registered at this point. Kicks off eagerly-initialized singletons + // (if requested; default behavior is to do so). + void registrationComplete(bool autoStartEagerInit = true) { RequestContext::saveContext(); std::atexit([](){ SingletonVault::singleton()->destroyInstances(); }); - RWSpinLock::WriteHolder wh(&stateMutex_); + { + RWSpinLock::WriteHolder wh(&stateMutex_); - stateCheck(SingletonVaultState::Running); + stateCheck(SingletonVaultState::Running); - if (type_ == Type::Strict) { - for (const auto& p: singletons_) { - if (p.second->hasLiveInstance()) { - throw std::runtime_error( - "Singleton created before registration was complete."); + if (type_ == Type::Strict) { + for (const auto& p: singletons_) { + if (p.second->hasLiveInstance()) { + throw std::runtime_error( + "Singleton created before registration was complete."); + } } } + + registrationComplete_ = true; + } + + if (autoStartEagerInit) { + startEagerInit(); + } + } + + /** + * If eagerInitExecutor_ is non-nullptr (default is nullptr) then + * schedule eager singletons' initializations through it. + * Otherwise, initializes them synchronously, in a loop. + */ + void startEagerInit() { + std::unordered_set singletonSet; + { + RWSpinLock::ReadHolder rh(&stateMutex_); + stateCheck(SingletonVaultState::Running); + if (UNLIKELY(!registrationComplete_)) { + throw std::logic_error( + "registrationComplete() not yet called"); + } + singletonSet = eagerInitSingletons_; // copy set of pointers } - registrationComplete_ = true; + auto *exe = eagerInitExecutor_; // default value is nullptr + for (auto *single : singletonSet) { + if (exe) { + eagerInitExecutor_->add([single] { + if (!single->creationStarted()) { + single->createInstance(); + } + }); + } else { + single->createInstance(); + } + } + } + + /** + * Provide an executor through which startEagerInit would run tasks. + * If there are several singletons which may be independently initialized, + * and their construction takes long, they could possibly be run in parallel + * to cut down on startup time. Unusual; default (synchronous initialization + * in a loop) is probably fine for most use cases, and most apps can most + * likely avoid using this. + */ + void setEagerInitExecutor(folly::Executor *exe) { + eagerInitExecutor_ = exe; } // Destroy all singletons; when complete, the vault can't create @@ -421,6 +518,8 @@ class SingletonVault { mutable folly::RWSpinLock mutex_; SingletonMap singletons_; + std::unordered_set eagerInitSingletons_; + folly::Executor* eagerInitExecutor_{nullptr}; std::vector creation_order_; SingletonVaultState state_{SingletonVaultState::Running}; bool registrationComplete_{false}; @@ -489,6 +588,30 @@ class Singleton { vault->registerSingleton(&getEntry()); } + /** + * Should be instantiated as soon as "registrationComplete()" is called. + * Singletons are usually lazy-loaded (built on-demand) but for those which + * are known to be needed, to avoid the potential lag for objects that take + * long to construct during runtime, there is an option to make sure these + * are built up-front. + * + * Use like: + * Singleton gFooInstance = Singleton(...).shouldEagerInit(); + * + * Or alternately, define the singleton as usual, and say + * gFooInstance.shouldEagerInit() + * + * at some point prior to calling registrationComplete(). + * Then registrationComplete can be called (by default it will kick off + * init of the eager singletons); alternately, you can use + * startEagerInit(). + */ + Singleton& shouldEagerInit() { + auto vault = SingletonVault::singleton(); + vault->addEagerInitSingleton(&getEntry()); + return *this; + } + /** * Construct and inject a mock singleton which should be used only from tests. * Unlike regular singletons which are initialized once per process lifetime, diff --git a/folly/test/SingletonTest.cpp b/folly/test/SingletonTest.cpp index f1847d84272..dbdb579b6ac 100644 --- a/folly/test/SingletonTest.cpp +++ b/folly/test/SingletonTest.cpp @@ -17,11 +17,13 @@ #include #include +#include #include #include #include +#include using namespace folly; @@ -482,6 +484,129 @@ TEST(Singleton, SingletonConcurrencyStress) { } } +namespace { +struct EagerInitSyncTag {}; +} +template +using SingletonEagerInitSync = Singleton; +TEST(Singleton, SingletonEagerInitSync) { + auto& vault = *SingletonVault::singleton(); + bool didEagerInit = false; + auto sing = SingletonEagerInitSync( + [&] {didEagerInit = true; return new std::string("foo"); }) + .shouldEagerInit(); + vault.registrationComplete(); + EXPECT_TRUE(didEagerInit); + sing.get_weak(); // (avoid compile error complaining about unused var 'sing') +} + +namespace { +struct EagerInitAsyncTag {}; +} +template +using SingletonEagerInitAsync = Singleton; +TEST(Singleton, SingletonEagerInitAsync) { + auto& vault = *SingletonVault::singleton(); + bool didEagerInit = false; + auto sing = SingletonEagerInitAsync( + [&] {didEagerInit = true; return new std::string("foo"); }) + .shouldEagerInit(); + folly::EventBase eb; + vault.setEagerInitExecutor(&eb); + vault.registrationComplete(); + EXPECT_FALSE(didEagerInit); + eb.loop(); + EXPECT_TRUE(didEagerInit); + sing.get_weak(); // (avoid compile error complaining about unused var 'sing') +} + +namespace { +class TestEagerInitParallelExecutor : public folly::Executor { + public: + explicit TestEagerInitParallelExecutor(const size_t threadCount) { + eventBases_.reserve(threadCount); + threads_.reserve(threadCount); + for (size_t i = 0; i < threadCount; i++) { + eventBases_.push_back(std::make_shared()); + auto eb = eventBases_.back(); + threads_.emplace_back(std::make_shared( + [eb] { eb->loopForever(); })); + } + } + + virtual ~TestEagerInitParallelExecutor() override { + for (auto eb : eventBases_) { + eb->runInEventBaseThread([eb] { eb->terminateLoopSoon(); }); + } + for (auto thread : threads_) { + thread->join(); + } + } + + virtual void add(folly::Func func) override { + const auto index = (counter_ ++) % eventBases_.size(); + eventBases_[index]->add(func); + } + + private: + std::vector> eventBases_; + std::vector> threads_; + std::atomic counter_ {0}; +}; +} // namespace + +namespace { +struct EagerInitParallelTag {}; +} +template +using SingletonEagerInitParallel = Singleton; +TEST(Singleton, SingletonEagerInitParallel) { + const static size_t kIters = 1000; + const static size_t kThreads = 20; + + std::atomic initCounter; + + auto& vault = *SingletonVault::singleton(); + + auto sing = SingletonEagerInitParallel( + [&] {++initCounter; return new std::string(""); }) + .shouldEagerInit(); + + for (size_t i = 0; i < kIters; i++) { + SCOPE_EXIT { + // clean up each time + vault.destroyInstances(); + vault.reenableInstances(); + }; + + initCounter.store(0); + + { + boost::barrier barrier(kThreads + 1); + TestEagerInitParallelExecutor exe(kThreads); + vault.setEagerInitExecutor(&exe); + vault.registrationComplete(false); + + EXPECT_EQ(0, initCounter.load()); + + for (size_t j = 0; j < kThreads; j++) { + exe.add([&] { + barrier.wait(); + vault.startEagerInit(); + barrier.wait(); + }); + } + + barrier.wait(); // to await all threads' readiness + barrier.wait(); // to await all threads' completion + } + + EXPECT_EQ(1, initCounter.load()); + + sing.get_weak(); // (avoid compile error complaining about unused var) + } +} + // Benchmarking a normal singleton vs a Meyers singleton vs a Folly // singleton. Meyers are insanely fast, but (hopefully) Folly // singletons are fast "enough."