/* * Copyright (c) Facebook, Inc. and its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include namespace folly { /* Base class for implementing threadpool based executors. * * Dynamic thread behavior: * * ThreadPoolExecutors may vary their actual running number of threads * between minThreads_ and maxThreads_, tracked by activeThreads_. * The actual implementation of joining an idle thread is left to the * ThreadPoolExecutors' subclass (typically by LifoSem try_take_for * timing out). Idle threads should be removed from threadList_, and * threadsToJoin incremented, and activeThreads_ decremented. * * On task add(), if an executor can garantee there is an active * thread that will handle the task, then nothing needs to be done. * If not, then ensureActiveThreads() should be called to possibly * start another pool thread, up to maxThreads_. * * ensureJoined() is called on add(), such that we can join idle * threads that were destroyed (which can't be joined from * themselves). * * Thread pool stats accounting: * * Derived classes must register instances to keep stats on all thread * pools by calling registerThreadPoolExecutor(this) on constructions * and deregisterThreadPoolExecutor(this) on destruction. * * Registration must be done wherever getPendingTaskCountImpl is implemented * and getPendingTaskCountImpl should be marked 'final' to avoid data races. */ class ThreadPoolExecutor : public DefaultKeepAliveExecutor { public: explicit ThreadPoolExecutor( size_t maxThreads, size_t minThreads, std::shared_ptr threadFactory, bool isWaitForAll = false); ~ThreadPoolExecutor() override; void add(Func func) override = 0; virtual void add(Func func, std::chrono::milliseconds expiration, Func expireCallback); void setThreadFactory(std::shared_ptr threadFactory) { CHECK(numThreads() == 0); threadFactory_ = std::move(threadFactory); } std::shared_ptr getThreadFactory() const { return threadFactory_; } size_t numThreads() const; void setNumThreads(size_t numThreads); // Return actual number of active threads -- this could be different from // numThreads() due to ThreadPoolExecutor's dynamic behavior. size_t numActiveThreads() const; /* * stop() is best effort - there is no guarantee that unexecuted tasks won't * be executed before it returns. Specifically, IOThreadPoolExecutor's stop() * behaves like join(). */ void stop(); void join(); /** * Execute f against all ThreadPoolExecutors, primarily for retrieving and * exporting stats. */ static void withAll(FunctionRef f); struct PoolStats { PoolStats() : threadCount(0), idleThreadCount(0), activeThreadCount(0), pendingTaskCount(0), totalTaskCount(0), maxIdleTime(0) {} size_t threadCount, idleThreadCount, activeThreadCount; uint64_t pendingTaskCount, totalTaskCount; std::chrono::nanoseconds maxIdleTime; }; PoolStats getPoolStats() const; size_t getPendingTaskCount() const; std::string getName() const; struct TaskStats { TaskStats() : expired(false), waitTime(0), runTime(0) {} bool expired; std::chrono::nanoseconds waitTime; std::chrono::nanoseconds runTime; }; using TaskStatsCallback = std::function; void subscribeToTaskStats(TaskStatsCallback cb); /** * Base class for threads created with ThreadPoolExecutor. * Some subclasses have methods that operate on these * handles. */ class ThreadHandle { public: virtual ~ThreadHandle() = default; }; /** * Observer interface for thread start/stop. * Provides hooks so actions can be taken when * threads are created */ class Observer { public: virtual void threadStarted(ThreadHandle*) = 0; virtual void threadStopped(ThreadHandle*) = 0; virtual void threadPreviouslyStarted(ThreadHandle* h) { threadStarted(h); } virtual void threadNotYetStopped(ThreadHandle* h) { threadStopped(h); } virtual ~Observer() = default; }; void addObserver(std::shared_ptr); void removeObserver(std::shared_ptr); void setThreadDeathTimeout(std::chrono::milliseconds timeout) { threadTimeout_ = timeout; } protected: // Prerequisite: threadListLock_ writelocked void addThreads(size_t n); // Prerequisite: threadListLock_ writelocked void removeThreads(size_t n, bool isJoin); struct TaskStatsCallbackRegistry; struct alignas(folly::cacheline_align_v) Thread : public ThreadHandle { explicit Thread(ThreadPoolExecutor* pool) : id(nextId++), handle(), idle(true), lastActiveTime(std::chrono::steady_clock::now()), taskStatsCallbacks(pool->taskStatsCallbacks_) {} ~Thread() override = default; static std::atomic nextId; uint64_t id; std::thread handle; bool idle; std::chrono::steady_clock::time_point lastActiveTime; folly::Baton<> startupBaton; std::shared_ptr taskStatsCallbacks; }; typedef std::shared_ptr ThreadPtr; struct Task { explicit Task( Func&& func, std::chrono::milliseconds expiration, Func&& expireCallback); Func func_; TaskStats stats_; std::chrono::steady_clock::time_point enqueueTime_; std::chrono::milliseconds expiration_; Func expireCallback_; std::shared_ptr context_; }; static void runTask(const ThreadPtr& thread, Task&& task); // The function that will be bound to pool threads. It must call // thread->startupBaton.post() when it's ready to consume work. virtual void threadRun(ThreadPtr thread) = 0; // Stop n threads and put their ThreadPtrs in the stoppedThreads_ queue // and remove them from threadList_, either synchronize or asynchronize // Prerequisite: threadListLock_ writelocked virtual void stopThreads(size_t n) = 0; // Join n stopped threads and remove them from waitingForJoinThreads_ queue. // Should not hold a lock because joining thread operation may invoke some // cleanup operations on the thread, and those cleanup operations may // require a lock on ThreadPoolExecutor. void joinStoppedThreads(size_t n); // Create a suitable Thread struct virtual ThreadPtr makeThread() { return std::make_shared(this); } static void registerThreadPoolExecutor(ThreadPoolExecutor* tpe); static void deregisterThreadPoolExecutor(ThreadPoolExecutor* tpe); // Prerequisite: threadListLock_ readlocked or writelocked virtual size_t getPendingTaskCountImpl() const = 0; class ThreadList { public: void add(const ThreadPtr& state) { auto it = std::lower_bound( vec_.begin(), vec_.end(), state, // compare method is a static method of class // and therefore cannot be inlined by compiler // as a template predicate of the STL algorithm // but wrapped up with the lambda function (lambda will be inlined) // compiler can inline compare method as well [&](const ThreadPtr& ts1, const ThreadPtr& ts2) -> bool { // inline return compare(ts1, ts2); }); vec_.insert(it, state); } void remove(const ThreadPtr& state) { auto itPair = std::equal_range( vec_.begin(), vec_.end(), state, // the same as above [&](const ThreadPtr& ts1, const ThreadPtr& ts2) -> bool { // inline return compare(ts1, ts2); }); CHECK(itPair.first != vec_.end()); CHECK(std::next(itPair.first) == itPair.second); vec_.erase(itPair.first); } const std::vector& get() const { return vec_; } private: static bool compare(const ThreadPtr& ts1, const ThreadPtr& ts2) { return ts1->id < ts2->id; } std::vector vec_; }; class StoppedThreadQueue : public BlockingQueue { public: BlockingQueueAddResult add(ThreadPtr item) override; ThreadPtr take() override; size_t size() override; folly::Optional try_take_for( std::chrono::milliseconds /*timeout */) override; private: folly::LifoSem sem_; std::mutex mutex_; std::queue queue_; }; std::shared_ptr threadFactory_; const bool isWaitForAll_; // whether to wait till event base loop exits ThreadList threadList_; SharedMutex threadListLock_; StoppedThreadQueue stoppedThreads_; std::atomic isJoin_{false}; // whether the current downsizing is a join struct TaskStatsCallbackRegistry { folly::ThreadLocal inCallback; folly::Synchronized> callbackList; }; std::shared_ptr taskStatsCallbacks_; std::vector> observers_; folly::ThreadPoolListHook threadPoolHook_; // Dynamic thread sizing functions and variables void ensureActiveThreads(); void ensureJoined(); bool minActive(); bool tryTimeoutThread(); // These are only modified while holding threadListLock_, but // are read without holding the lock. std::atomic maxThreads_{0}; std::atomic minThreads_{0}; std::atomic activeThreads_{0}; std::atomic threadsToJoin_{0}; std::chrono::milliseconds threadTimeout_{0}; void joinKeepAliveOnce() { if (!std::exchange(keepAliveJoined_, true)) { joinKeepAlive(); } } bool keepAliveJoined_{false}; }; } // namespace folly