Commit be42394a authored by Chip Turner's avatar Chip Turner Committed by Sara Golemon

Fix bug in circular singleton creation detection

Summary:
We considered it circular if we tried to create a singleton
while the singleton was being created.  In a single threaded world, this
is correct, but under concurrency, two threads can be in the singleton
creation codepath and become confused about the state of the singleton.

This change uses a condition variable to notify when creation completes
so that other threads can wait on the creation to complete.  Circular
creation is detected via thread id.

Test Plan:
runtests (new test case; failed without the fix, passes with
it)

Reviewed By: hans@fb.com

Subscribers: lins, anca, njormrod, rkroll

FB internal diff: D1534081
parent 2bfde4cd
...@@ -33,7 +33,7 @@ void SingletonVault::destroyInstances() { ...@@ -33,7 +33,7 @@ void SingletonVault::destroyInstances() {
auto it = singletons_.find(type); auto it = singletons_.find(type);
CHECK(it != singletons_.end()); CHECK(it != singletons_.end());
auto& entry = it->second; auto& entry = it->second;
std::lock_guard<std::mutex> entry_guard(entry->mutex_); std::lock_guard<std::mutex> entry_guard(entry->mutex);
if (entry->instance.use_count() > 1) { if (entry->instance.use_count() > 1) {
LOG(ERROR) << "Singleton of type " << type.name() << " has a living " LOG(ERROR) << "Singleton of type " << type.name() << " has a living "
<< "reference at destroyInstances time; beware! Raw pointer " << "reference at destroyInstances time; beware! Raw pointer "
...@@ -42,6 +42,7 @@ void SingletonVault::destroyInstances() { ...@@ -42,6 +42,7 @@ void SingletonVault::destroyInstances() {
} }
entry->instance.reset(); entry->instance.reset();
entry->state = SingletonEntryState::Dead; entry->state = SingletonEntryState::Dead;
entry->state_condvar.notify_all();
} }
creation_order_.clear(); creation_order_.clear();
......
...@@ -81,6 +81,8 @@ ...@@ -81,6 +81,8 @@
#include <vector> #include <vector>
#include <mutex> #include <mutex>
#include <thread>
#include <condition_variable>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <functional> #include <functional>
...@@ -181,7 +183,7 @@ class SingletonVault { ...@@ -181,7 +183,7 @@ class SingletonVault {
entry.reset(new SingletonEntry); entry.reset(new SingletonEntry);
} }
std::lock_guard<std::mutex> entry_guard(entry->mutex_); std::lock_guard<std::mutex> entry_guard(entry->mutex);
CHECK(entry->instance == nullptr); CHECK(entry->instance == nullptr);
CHECK(create); CHECK(create);
CHECK(teardown); CHECK(teardown);
...@@ -261,12 +263,21 @@ class SingletonVault { ...@@ -261,12 +263,21 @@ class SingletonVault {
// its state as described above, and the create and teardown // its state as described above, and the create and teardown
// functions. // functions.
struct SingletonEntry { struct SingletonEntry {
std::mutex mutex_; // mutex protects the entire entry
std::mutex mutex;
// state changes notify state_condvar
SingletonEntryState state = SingletonEntryState::Dead;
std::condition_variable state_condvar;
// the thread creating the singleton
std::thread::id creating_thread;
// The singleton itself and related functions.
std::shared_ptr<void> instance; std::shared_ptr<void> instance;
void* instance_ptr = nullptr; void* instance_ptr = nullptr;
CreateFunc create = nullptr; CreateFunc create = nullptr;
TeardownFunc teardown = nullptr; TeardownFunc teardown = nullptr;
SingletonEntryState state = SingletonEntryState::Dead;
SingletonEntry() = default; SingletonEntry() = default;
SingletonEntry(const SingletonEntry&) = delete; SingletonEntry(const SingletonEntry&) = delete;
...@@ -280,7 +291,7 @@ class SingletonVault { ...@@ -280,7 +291,7 @@ class SingletonVault {
// necessary. // necessary.
SingletonEntry* get_entry(detail::TypeDescriptor type, SingletonEntry* get_entry(detail::TypeDescriptor type,
std::unique_lock<std::mutex>* lock) { std::unique_lock<std::mutex>* lock) {
// mutex_ must be held when calling this function // mutex must be held when calling this function
if (state_ != SingletonVaultState::Running) { if (state_ != SingletonVaultState::Running) {
throw std::logic_error( throw std::logic_error(
"Attempt to load a singleton before " "Attempt to load a singleton before "
...@@ -294,17 +305,31 @@ class SingletonVault { ...@@ -294,17 +305,31 @@ class SingletonVault {
type.name()); type.name());
} }
auto& entry = it->second; auto entry = it->second.get();
std::unique_lock<std::mutex> entry_lock(entry->mutex_); std::unique_lock<std::mutex> entry_lock(entry->mutex);
if (entry->state == SingletonEntryState::BeingBorn) { if (entry->state == SingletonEntryState::BeingBorn) {
throw std::out_of_range(std::string("circular singleton dependency: ") + // If this thread is trying to give birth to the singleton, it's
type.name()); // a circular dependency and we must panic.
if (entry->creating_thread == std::this_thread::get_id()) {
throw std::out_of_range(std::string("circular singleton dependency: ") +
type.name());
}
// Otherwise, another thread is constructing the singleton;
// let's wait on a condvar to see it complete. We release and
// reaquire lock while waiting on the entry to resolve its state.
lock->unlock();
entry->state_condvar.wait(entry_lock, [&entry]() {
return entry->state != SingletonEntryState::BeingBorn;
});
lock->lock();
} }
if (entry->instance == nullptr) { if (entry->instance == nullptr) {
CHECK(entry->state == SingletonEntryState::Dead); CHECK(entry->state == SingletonEntryState::Dead);
entry->state = SingletonEntryState::BeingBorn; entry->state = SingletonEntryState::BeingBorn;
entry->creating_thread = std::this_thread::get_id();
entry_lock.unlock(); entry_lock.unlock();
lock->unlock(); lock->unlock();
...@@ -317,11 +342,12 @@ class SingletonVault { ...@@ -317,11 +342,12 @@ class SingletonVault {
entry->instance = instance; entry->instance = instance;
entry->instance_ptr = instance.get(); entry->instance_ptr = instance.get();
entry->state = SingletonEntryState::Living; entry->state = SingletonEntryState::Living;
entry->state_condvar.notify_all();
creation_order_.push_back(type); creation_order_.push_back(type);
} }
CHECK(entry->state == SingletonEntryState::Living); CHECK(entry->state == SingletonEntryState::Living);
return entry.get(); return entry;
} }
mutable std::mutex mutex_; mutable std::mutex mutex_;
......
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
#include <thread>
#include <folly/experimental/Singleton.h> #include <folly/experimental/Singleton.h>
#include <folly/Benchmark.h> #include <folly/Benchmark.h>
...@@ -307,6 +309,42 @@ TEST(Singleton, SingletonDependencies) { ...@@ -307,6 +309,42 @@ TEST(Singleton, SingletonDependencies) {
std::out_of_range); std::out_of_range);
} }
// A test to ensure multiple threads contending on singleton creation
// properly wait for creation rather than thinking it is a circular
// dependency.
class Slowpoke {
public:
Slowpoke() { std::this_thread::sleep_for(std::chrono::seconds(1)); }
};
TEST(Singleton, SingletonConcurrency) {
SingletonVault vault;
Singleton<Slowpoke> slowpoke_singleton(nullptr, nullptr, &vault);
vault.registrationComplete();
std::mutex gatekeeper;
gatekeeper.lock();
auto func = [&vault, &gatekeeper]() {
gatekeeper.lock();
gatekeeper.unlock();
auto unused = Singleton<Slowpoke>::get(&vault);
};
EXPECT_EQ(vault.livingSingletonCount(), 0);
std::vector<std::thread> threads;
for (int i = 0; i < 100; ++i) {
threads.emplace_back(func);
}
// If circular dependency checks fail, the unlock would trigger a
// crash. Instead, it succeeds, and we have exactly one living
// singleton.
gatekeeper.unlock();
for (auto& t : threads) {
t.join();
}
EXPECT_EQ(vault.livingSingletonCount(), 1);
}
// Benchmarking a normal singleton vs a Meyers singleton vs a Folly // Benchmarking a normal singleton vs a Meyers singleton vs a Folly
// singleton. Meyers are insanely fast, but (hopefully) Folly // singleton. Meyers are insanely fast, but (hopefully) Folly
// singletons are fast "enough." // singletons are fast "enough."
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment