Simplify ThreadedRepeatingFunctionRunner by requiring classes that contain it to...
authorAlexey Spiridonov <lesha@fb.com>
Tue, 16 May 2017 21:05:49 +0000 (14:05 -0700)
committerFacebook Github Bot <facebook-github-bot@users.noreply.github.com>
Tue, 16 May 2017 21:20:21 +0000 (14:20 -0700)
Summary: It is pretty confusing to inherit from classes that manage threads. See the docblocks in this diff.

Reviewed By: yfeldblum

Differential Revision: D4973498

fbshipit-source-id: 2fcf1ddf68ef46d4d78a9b40f304262064862715

folly/experimental/ThreadedRepeatingFunctionRunner.cpp
folly/experimental/ThreadedRepeatingFunctionRunner.h

index 56a15c9..a309f01 100644 (file)
@@ -23,20 +23,13 @@ namespace folly {
 ThreadedRepeatingFunctionRunner::ThreadedRepeatingFunctionRunner() {}
 
 ThreadedRepeatingFunctionRunner::~ThreadedRepeatingFunctionRunner() {
-  stopAndWarn("ThreadedRepeatingFunctionRunner");
-}
-
-void ThreadedRepeatingFunctionRunner::stopAndWarn(
-    const std::string& class_of_destructor) {
   if (stopImpl()) {
     LOG(ERROR)
         << "ThreadedRepeatingFunctionRunner::stop() should already have been "
-        << "called, since the " << class_of_destructor << " destructor is now "
-        << "running. This is unsafe because it means that its threads "
-        << "may be accessing class state that was already destroyed "
-        << "(e.g. derived class members, or members that were declared after "
-        << "the " << class_of_destructor << ") .";
-    stop();
+        << "called, since we are now in the Runner's destructor. This is "
+        << "because it means that its threads may be accessing object state "
+        << "that was already destroyed -- e.g. members that were declared "
+        << "after the ThreadedRepeatingFunctionRunner.";
   }
 }
 
index 878da72..75122da 100644 (file)
@@ -51,32 +51,38 @@ namespace folly {
  * if you want to have ThreadedRepeatingFunctionRunner as a member of your
  * class.  A reasonable pattern looks like this:
  *
- * struct MyClass {
+ * // Your class must be `final` because inheriting from a class with
+ * // threads can cause all sorts of subtle issues:
+ * //  - Your base class might start threads that attempt to access derived
+ * //    class state **before** that state was constructed.
+ * //  - Your base class's destructor will only be able to stop threads
+ * //    **after** the derived class state was destroyed -- and that state
+ * //    might be accessed by the threads.
+ * // In short, any derived class would have to do work to manage the
+ * // threads itself, which makes inheritance a poor means of composition.
+ * struct MyClass final {
  *   // Note that threads are NOT added in the constructor, for two reasons:
  *   //
- *   //   (1) If you added some, and had any subsequent initialization (e.g.
- *   //       derived class constructors), 'this' would not be fully
- *   //       constructed when the worker threads came up, causing
- *   //       heisenbugs.
+ *   //   (1) If you first added some threads, and then had additional
+ *   //       initialization (e.g. derived class constructors), `this` might
+ *   //       not be fully constructed by the time the function threads
+ *   //       started running, causing heisenbugs.
  *   //
- *   //   (2) Also, if your constructor threw after thread creation, the
- *   //       class destructor would not be invoked, potentially leaving the
+ *   //   (2) If your constructor threw after thread creation, the class
+ *   //       destructor would not be invoked, potentially leaving the
  *   //       threads running too long.
  *   //
- *   // It's better to have explicit two-step initialization, or to lazily
- *   // add threads the first time they are needed.
+ *   // It is much safer to have explicit two-step initialization, or to
+ *   // lazily add threads the first time they are needed.
  *   MyClass() : count_(0) {}
  *
  *   // You must stop the threads as early as possible in the destruction
- *   // process (or even before).  In the case of a class hierarchy, the
- *   // final class MUST always call stop() as the first thing in its
- *   // destructor -- otherwise, the worker threads may access already-
+ *   // process (or even before).  If MyClass had derived classes, the final
+ *   // derived class MUST always call stop() as the first thing in its
+ *   // destructor -- otherwise, the worker threads might access already-
  *   // destroyed state.
  *   ~MyClass() {
- *     // if MyClass is abstract:
- *     threads_.stopAndWarn("MyClass");
- *     // Otherwise:
- *     threads_.stop();
+ *     threads_.stop();  // Stop threads BEFORE destroying any state they use.
  *   }
  *
  *   // See the constructor for why two-stage initialization is preferred.
@@ -91,7 +97,7 @@ namespace folly {
  *
  * private:
  *   std::atomic<int> count_;
- *   // Declared last because the threads' functions access other members.
+ *   // CAUTION: Declare last since the threads access other members of `this`.
  *   ThreadedRepeatingFunctionRunner threads_;
  * };
  */
@@ -111,14 +117,6 @@ class ThreadedRepeatingFunctionRunner final {
    */
   void stop();
 
-  /**
-   * Must be called at the TOP of the destructor of any abstract class that
-   * contains ThreadedRepeatingFunctionRunner (directly or through a
-   * parent).  Any non-abstract class destructor must instead stop() at the
-   * top.
-   */
-  void stopAndWarn(const std::string& class_of_destructor);
-
   /**
    * Run your noexcept function `f` in a background loop, sleeping between
    * calls for a duration returned by `f`.  Optionally waits for