Commit 206dce5b authored by Philip Pronin's avatar Philip Pronin Committed by Facebook Github Bot

fix race between StaticMetaBase::destroy() and StaticMetaBase::onThreadExit()

Summary:
We would like to guarantee that after `folly::ThreadLocal<>` dtor
returns no per-thread instances are longer alive.  Currently this is not a case:

* T1 is excuting `StaticMetaBase::onThreadExit()`, it acquired all per-thread
  instances and erased them from meta under `accessAllThreadsLock_`.
* T2 destroys `folly::ThreadLocal<>`, it executes `StaticMetaBase::destroy()`,
  collects all per-thread instances (thus missing the ones being destroyed by
  T1), destroys them and returns.
* T1 executes dtor of per-thread instances, after parent `folly::ThreadLocal<>`
  dtor is finished.

Reviewed By: ot

Differential Revision: D4109820

fbshipit-source-id: d547b8cc77c9871126538c38644c2e98ddccf220
parent 5e1b7327
...@@ -47,7 +47,7 @@ class ThreadCachedInt : boost::noncopyable { ...@@ -47,7 +47,7 @@ class ThreadCachedInt : boost::noncopyable {
void increment(IntT inc) { void increment(IntT inc) {
auto cache = cache_.get(); auto cache = cache_.get();
if (UNLIKELY(cache == nullptr || cache->parent_ == nullptr)) { if (UNLIKELY(cache == nullptr)) {
cache = new IntCache(*this); cache = new IntCache(*this);
cache_.reset(cache); cache_.reset(cache);
} }
...@@ -122,15 +122,6 @@ class ThreadCachedInt : boost::noncopyable { ...@@ -122,15 +122,6 @@ class ThreadCachedInt : boost::noncopyable {
target_.store(newVal, std::memory_order_release); target_.store(newVal, std::memory_order_release);
} }
// This is a little tricky - it's possible that our IntCaches are still alive
// in another thread and will get destroyed after this destructor runs, so we
// need to make sure we signal that this parent is dead.
~ThreadCachedInt() {
for (auto& cache : cache_.accessAllThreads()) {
cache.parent_ = nullptr;
}
}
private: private:
std::atomic<IntT> target_; std::atomic<IntT> target_;
std::atomic<uint32_t> cacheSize_; std::atomic<uint32_t> cacheSize_;
...@@ -173,9 +164,7 @@ class ThreadCachedInt : boost::noncopyable { ...@@ -173,9 +164,7 @@ class ThreadCachedInt : boost::noncopyable {
} }
~IntCache() { ~IntCache() {
if (parent_) { flush();
flush();
}
} }
}; };
}; };
......
...@@ -98,38 +98,54 @@ uint32_t StaticMetaBase::allocate(EntryID* ent) { ...@@ -98,38 +98,54 @@ uint32_t StaticMetaBase::allocate(EntryID* ent) {
void StaticMetaBase::destroy(EntryID* ent) { void StaticMetaBase::destroy(EntryID* ent) {
try { try {
auto& meta = *this; auto& meta = *this;
// Elements in other threads that use this id. // Elements in other threads that use this id.
std::vector<ElementWrapper> elements; std::vector<ElementWrapper> elements;
{ {
std::lock_guard<std::mutex> g(meta.lock_); SharedMutex::WriteHolder wlock;
uint32_t id = ent->value.exchange(kEntryIDInvalid); if (meta.strict_) {
if (id == kEntryIDInvalid) { /*
return; * In strict mode, the logic guarantees per-thread instances are
* destroyed by the moment ThreadLocal<> dtor returns.
* In order to achieve that, we should wait until concurrent
* onThreadExit() calls (that might acquire ownership over per-thread
* instances in order to destroy them) are finished.
*/
wlock = SharedMutex::WriteHolder(meta.accessAllThreadsLock_);
} }
for (ThreadEntry* e = meta.head_.next; e != &meta.head_; e = e->next) { {
if (id < e->elementsCapacity && e->elements[id].ptr) { std::lock_guard<std::mutex> g(meta.lock_);
elements.push_back(e->elements[id]); uint32_t id = ent->value.exchange(kEntryIDInvalid);
if (id == kEntryIDInvalid) {
/* return;
* Writing another thread's ThreadEntry from here is fine; }
* the only other potential reader is the owning thread --
* from onThreadExit (which grabs the lock, so is properly for (ThreadEntry* e = meta.head_.next; e != &meta.head_; e = e->next) {
* synchronized with us) or from get(), which also grabs if (id < e->elementsCapacity && e->elements[id].ptr) {
* the lock if it needs to resize the elements vector. elements.push_back(e->elements[id]);
*
* We can't conflict with reads for a get(id), because /*
* it's illegal to call get on a thread local that's * Writing another thread's ThreadEntry from here is fine;
* destructing. * the only other potential reader is the owning thread --
*/ * from onThreadExit (which grabs the lock, so is properly
e->elements[id].ptr = nullptr; * synchronized with us) or from get(), which also grabs
e->elements[id].deleter1 = nullptr; * the lock if it needs to resize the elements vector.
e->elements[id].ownsDeleter = false; *
* We can't conflict with reads for a get(id), because
* it's illegal to call get on a thread local that's
* destructing.
*/
e->elements[id].ptr = nullptr;
e->elements[id].deleter1 = nullptr;
e->elements[id].ownsDeleter = false;
}
} }
meta.freeIds_.push_back(id);
} }
meta.freeIds_.push_back(id);
} }
// Delete elements outside the lock // Delete elements outside the locks.
for (ElementWrapper& elem : elements) { for (ElementWrapper& elem : elements) {
elem.dispose(TLPDestructionMode::ALL_THREADS); elem.dispose(TLPDestructionMode::ALL_THREADS);
} }
......
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