/*
- * Copyright 2012 Facebook, Inc.
+ * Copyright 2016 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* limitations under the License.
*/
-#include "folly/ThreadLocal.h"
+#include <folly/ThreadLocal.h>
-#include <map>
-#include <unordered_map>
-#include <set>
+#include <dlfcn.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <array>
#include <atomic>
-#include <mutex>
+#include <chrono>
#include <condition_variable>
+#include <limits.h>
+#include <map>
+#include <mutex>
+#include <set>
#include <thread>
-#include <boost/thread/tss.hpp>
-#include <gtest/gtest.h>
-#include <gflags/gflags.h>
+#include <unordered_map>
+
#include <glog/logging.h>
-#include "folly/Benchmark.h"
+#include <gtest/gtest.h>
+
+#include <folly/Baton.h>
+#include <folly/Memory.h>
+#include <folly/experimental/io/FsUtil.h>
+#include <folly/portability/Unistd.h>
using namespace folly;
}
static void customDeleter(Widget* w, TLPDestructionMode mode) {
- totalVal_ += (mode == TLPDestructionMode::ALL_THREADS) * 1000;
+ totalVal_ += (mode == TLPDestructionMode::ALL_THREADS) ? 1000 : 1;
delete w;
}
};
w.reset(new Widget(), Widget::customDeleter);
w.get()->val_ += 10;
}).join();
+ EXPECT_EQ(11, Widget::totalVal_);
+ }
+ EXPECT_EQ(11, Widget::totalVal_);
+}
+
+TEST(ThreadLocalPtr, CustomDeleterOwnershipTransfer) {
+ Widget::totalVal_ = 0;
+ {
+ ThreadLocalPtr<Widget> w;
+ auto deleter = [](Widget* ptr) {
+ Widget::customDeleter(ptr, TLPDestructionMode::THIS_THREAD);
+ };
+ std::unique_ptr<Widget, typeof(deleter)> source(new Widget(), deleter);
+ std::thread([&w, &source]() {
+ w.reset(std::move(source));
+ w.get()->val_ += 10;
+ }).join();
+ EXPECT_EQ(11, Widget::totalVal_);
+ }
+ EXPECT_EQ(11, Widget::totalVal_);
+}
+
+TEST(ThreadLocalPtr, DefaultDeleterOwnershipTransfer) {
+ Widget::totalVal_ = 0;
+ {
+ ThreadLocalPtr<Widget> w;
+ auto source = folly::make_unique<Widget>();
+ std::thread([&w, &source]() {
+ w.reset(std::move(source));
+ w.get()->val_ += 10;
+ }).join();
EXPECT_EQ(10, Widget::totalVal_);
}
EXPECT_EQ(10, Widget::totalVal_);
EXPECT_FALSE(tl);
}
+TEST(ThreadLocalPtr, TestRelease) {
+ Widget::totalVal_ = 0;
+ ThreadLocalPtr<Widget> w;
+ std::unique_ptr<Widget> wPtr;
+ std::thread([&w, &wPtr]() {
+ w.reset(new Widget());
+ w.get()->val_ += 10;
+
+ wPtr.reset(w.release());
+ }).join();
+ EXPECT_EQ(0, Widget::totalVal_);
+ wPtr.reset();
+ EXPECT_EQ(10, Widget::totalVal_);
+}
+
+TEST(ThreadLocalPtr, CreateOnThreadExit) {
+ Widget::totalVal_ = 0;
+ ThreadLocal<Widget> w;
+ ThreadLocalPtr<int> tl;
+
+ std::thread([&] {
+ tl.reset(new int(1),
+ [&](int* ptr, TLPDestructionMode /* mode */) {
+ delete ptr;
+ // This test ensures Widgets allocated here are not leaked.
+ ++w.get()->val_;
+ ThreadLocal<Widget> wl;
+ ++wl.get()->val_;
+ });
+ }).join();
+ EXPECT_EQ(2, Widget::totalVal_);
+}
+
// Test deleting the ThreadLocalPtr object
TEST(ThreadLocalPtr, CustomDeleter2) {
Widget::totalVal_ = 0;
TEST(ThreadLocal, InterleavedDestructors) {
Widget::totalVal_ = 0;
- ThreadLocal<Widget>* w = NULL;
+ std::unique_ptr<ThreadLocal<Widget>> w;
int wVersion = 0;
const int wVersionMax = 2;
int thIter = 0;
{
std::lock_guard<std::mutex> g(lock);
thIterPrev = thIter;
- delete w;
- w = new ThreadLocal<Widget>();
+ w.reset(new ThreadLocal<Widget>());
++wVersion;
}
while (true) {
EXPECT_EQ(4, tls.size());
}
-// Simple reference implementation using pthread_get_specific
-template<typename T>
-class PThreadGetSpecific {
+namespace {
+
+constexpr size_t kFillObjectSize = 300;
+
+std::atomic<uint64_t> gDestroyed;
+
+/**
+ * Fill a chunk of memory with a unique-ish pattern that includes the thread id
+ * (so deleting one of these from another thread would cause a failure)
+ *
+ * Verify it explicitly and on destruction.
+ */
+class FillObject {
public:
- PThreadGetSpecific() : key_(0) {
- pthread_key_create(&key_, OnThreadExit);
+ explicit FillObject(uint64_t idx) : idx_(idx) {
+ uint64_t v = val();
+ for (size_t i = 0; i < kFillObjectSize; ++i) {
+ data_[i] = v;
+ }
}
- T* get() const {
- return static_cast<T*>(pthread_getspecific(key_));
+ void check() {
+ uint64_t v = val();
+ for (size_t i = 0; i < kFillObjectSize; ++i) {
+ CHECK_EQ(v, data_[i]);
+ }
+ }
+
+ ~FillObject() {
+ ++gDestroyed;
}
- void reset(T* t) {
- delete get();
- pthread_setspecific(key_, t);
+ private:
+ uint64_t val() const {
+ return (idx_ << 40) | uint64_t(pthread_self());
+ }
+
+ uint64_t idx_;
+ uint64_t data_[kFillObjectSize];
+};
+
+} // namespace
+
+#if FOLLY_HAVE_STD_THIS_THREAD_SLEEP_FOR
+TEST(ThreadLocal, Stress) {
+ constexpr size_t numFillObjects = 250;
+ std::array<ThreadLocalPtr<FillObject>, numFillObjects> objects;
+
+ constexpr size_t numThreads = 32;
+ constexpr size_t numReps = 20;
+
+ std::vector<std::thread> threads;
+ threads.reserve(numThreads);
+
+ for (size_t i = 0; i < numThreads; ++i) {
+ threads.emplace_back([&objects] {
+ for (size_t rep = 0; rep < numReps; ++rep) {
+ for (size_t i = 0; i < objects.size(); ++i) {
+ objects[i].reset(new FillObject(rep * objects.size() + i));
+ std::this_thread::sleep_for(std::chrono::microseconds(100));
+ }
+ for (size_t i = 0; i < objects.size(); ++i) {
+ objects[i]->check();
+ }
+ }
+ });
}
- static void OnThreadExit(void* obj) {
- delete static_cast<T*>(obj);
+
+ for (auto& t : threads) {
+ t.join();
}
+
+ EXPECT_EQ(numFillObjects * numThreads * numReps, gDestroyed);
+}
+#endif
+
+// Yes, threads and fork don't mix
+// (http://cppwisdom.quora.com/Why-threads-and-fork-dont-mix) but if you're
+// stupid or desperate enough to try, we shouldn't stand in your way.
+namespace {
+class HoldsOne {
+ public:
+ HoldsOne() : value_(1) { }
+ // Do an actual access to catch the buggy case where this == nullptr
+ int value() const { return value_; }
private:
- pthread_key_t key_;
+ int value_;
};
-DEFINE_int32(numThreads, 8, "Number simultaneous threads for benchmarks.");
-
-#define REG(var) \
- BENCHMARK(FB_CONCATENATE(BM_mt_, var), iters) { \
- const int itersPerThread = iters / FLAGS_numThreads; \
- std::vector<std::thread> threads; \
- for (int i = 0; i < FLAGS_numThreads; ++i) { \
- threads.push_back(std::thread([&]() { \
- var.reset(new int(0)); \
- for (int i = 0; i < itersPerThread; ++i) { \
- ++(*var.get()); \
- } \
- })); \
- } \
- for (auto& t : threads) { \
- t.join(); \
- } \
+struct HoldsOneTag {};
+
+ThreadLocal<HoldsOne, HoldsOneTag> ptr;
+
+int totalValue() {
+ int value = 0;
+ for (auto& p : ptr.accessAllThreads()) {
+ value += p.value();
+ }
+ return value;
+}
+
+} // namespace
+
+#ifdef FOLLY_HAVE_PTHREAD_ATFORK
+TEST(ThreadLocal, Fork) {
+ EXPECT_EQ(1, ptr->value()); // ensure created
+ EXPECT_EQ(1, totalValue());
+ // Spawn a new thread
+
+ std::mutex mutex;
+ bool started = false;
+ std::condition_variable startedCond;
+ bool stopped = false;
+ std::condition_variable stoppedCond;
+
+ std::thread t([&] () {
+ EXPECT_EQ(1, ptr->value()); // ensure created
+ {
+ std::unique_lock<std::mutex> lock(mutex);
+ started = true;
+ startedCond.notify_all();
+ }
+ {
+ std::unique_lock<std::mutex> lock(mutex);
+ while (!stopped) {
+ stoppedCond.wait(lock);
+ }
+ }
+ });
+
+ {
+ std::unique_lock<std::mutex> lock(mutex);
+ while (!started) {
+ startedCond.wait(lock);
+ }
+ }
+
+ EXPECT_EQ(2, totalValue());
+
+ pid_t pid = fork();
+ if (pid == 0) {
+ // in child
+ int v = totalValue();
+
+ // exit successfully if v == 1 (one thread)
+ // diagnostic error code otherwise :)
+ switch (v) {
+ case 1: _exit(0);
+ case 0: _exit(1);
+ }
+ _exit(2);
+ } else if (pid > 0) {
+ // in parent
+ int status;
+ EXPECT_EQ(pid, waitpid(pid, &status, 0));
+ EXPECT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+ } else {
+ EXPECT_TRUE(false) << "fork failed";
}
-ThreadLocalPtr<int> tlp;
-REG(tlp);
-PThreadGetSpecific<int> pthread_get_specific;
-REG(pthread_get_specific);
-boost::thread_specific_ptr<int> boost_tsp;
-REG(boost_tsp);
-BENCHMARK_DRAW_LINE();
-
-int main(int argc, char** argv) {
- testing::InitGoogleTest(&argc, argv);
- google::ParseCommandLineFlags(&argc, &argv, true);
- google::SetCommandLineOptionWithMode(
- "bm_max_iters", "100000000", google::SET_FLAG_IF_DEFAULT
- );
- if (FLAGS_benchmark) {
- folly::runBenchmarks();
+ EXPECT_EQ(2, totalValue());
+
+ {
+ std::unique_lock<std::mutex> lock(mutex);
+ stopped = true;
+ stoppedCond.notify_all();
}
- return RUN_ALL_TESTS();
+
+ t.join();
+
+ EXPECT_EQ(1, totalValue());
}
+#endif
-/*
-Ran with 24 threads on dual 12-core Xeon(R) X5650 @ 2.67GHz with 12-MB caches
-
-Benchmark Iters Total t t/iter iter/sec
-------------------------------------------------------------------------------
-* BM_mt_tlp 100000000 39.88 ms 398.8 ps 2.335 G
- +5.91% BM_mt_pthread_get_specific 100000000 42.23 ms 422.3 ps 2.205 G
- + 295% BM_mt_boost_tsp 100000000 157.8 ms 1.578 ns 604.5 M
-------------------------------------------------------------------------------
-*/
+struct HoldsOneTag2 {};
+
+TEST(ThreadLocal, Fork2) {
+ // A thread-local tag that was used in the parent from a *different* thread
+ // (but not the forking thread) would cause the child to hang in a
+ // ThreadLocalPtr's object destructor. Yeah.
+ ThreadLocal<HoldsOne, HoldsOneTag2> p;
+ {
+ // use tag in different thread
+ std::thread t([&p] { p.get(); });
+ t.join();
+ }
+ pid_t pid = fork();
+ if (pid == 0) {
+ {
+ ThreadLocal<HoldsOne, HoldsOneTag2> q;
+ q.get();
+ }
+ _exit(0);
+ } else if (pid > 0) {
+ int status;
+ EXPECT_EQ(pid, waitpid(pid, &status, 0));
+ EXPECT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+ } else {
+ EXPECT_TRUE(false) << "fork failed";
+ }
+}
+
+TEST(ThreadLocal, SharedLibrary) {
+ auto exe = fs::executable_path();
+ auto lib = exe.parent_path() / "lib_thread_local_test.so";
+ auto handle = dlopen(lib.string().c_str(), RTLD_LAZY);
+ EXPECT_NE(nullptr, handle);
+
+ typedef void (*useA_t)();
+ dlerror();
+ useA_t useA = (useA_t) dlsym(handle, "useA");
+
+ const char *dlsym_error = dlerror();
+ EXPECT_EQ(nullptr, dlsym_error);
+
+ useA();
+
+ folly::Baton<> b11, b12, b21, b22;
+
+ std::thread t1([&]() {
+ useA();
+ b11.post();
+ b12.wait();
+ });
+
+ std::thread t2([&]() {
+ useA();
+ b21.post();
+ b22.wait();
+ });
+
+ b11.wait();
+ b21.wait();
+
+ dlclose(handle);
+
+ b12.post();
+ b22.post();
+
+ t1.join();
+ t2.join();
+}
+
+namespace folly { namespace threadlocal_detail {
+struct PthreadKeyUnregisterTester {
+ PthreadKeyUnregister p;
+ constexpr PthreadKeyUnregisterTester() = default;
+};
+}}
+
+TEST(ThreadLocal, UnregisterClassHasConstExprCtor) {
+ folly::threadlocal_detail::PthreadKeyUnregisterTester x;
+ // yep!
+ SUCCEED();
+}