(wangle) Future/Promise detachment
authorHans Fugal <fugalh@fb.com>
Mon, 30 Jun 2014 20:38:41 +0000 (13:38 -0700)
committerTudor Bosman <tudorb@fb.com>
Mon, 7 Jul 2014 15:42:20 +0000 (08:42 -0700)
Summary:
Bring a bit more sanity to the lifetime. Now Future and Promise detach by calling the respective methods on State, and they do it during their destruction only. State manages its own lifetime.

Besides being cleaner, this also sets the stage for cancellation (where we'll need Future to hang on to its reference to State after setting the callback), and for folding in Later (we will have a bool for whether the future is hot and hold off executing the callback if it isn't).

Also cleans up various things I noticed while auditing the code for usages of `state_`.

Test Plan:
All the unit tests still pass.
Ran with ASAN (and found I had introduced a bug, then fixed it. yay)

Reviewed By: hannesr@fb.com

Subscribers: jsedgwick, net-systems@, fugalh, exa

FB internal diff: D1412038

Tasks: 4618297

folly/wangle/Future-inl.h
folly/wangle/Future.h
folly/wangle/Promise-inl.h
folly/wangle/Promise.h
folly/wangle/detail/State.h
folly/wangle/test/FutureTest.cpp

index c3002f48e15eea54bc55f8dbbba3f56d6e1dab15..1e17e7cb40d4c3c8769a7caa35fe0200df40b6c2 100644 (file)
@@ -32,8 +32,8 @@ struct isFuture<Future<T> > {
 };
 
 template <class T>
-Future<T>::Future(Future<T>&& other) noexcept : state_(other.state_) {
-  other.state_ = nullptr;
+Future<T>::Future(Future<T>&& other) noexcept : state_(nullptr) {
+  *this = std::move(other);
 }
 
 template <class T>
@@ -44,8 +44,14 @@ Future<T>& Future<T>::operator=(Future<T>&& other) {
 
 template <class T>
 Future<T>::~Future() {
+  detach();
+}
+
+template <class T>
+void Future<T>::detach() {
   if (state_) {
-    setCallback_([](Try<T>&&) {}); // detach
+    state_->detachFuture();
+    state_ = nullptr;
   }
 }
 
@@ -60,7 +66,6 @@ template <class F>
 void Future<T>::setCallback_(F&& func) {
   throwIfInvalid();
   state_->setCallback(std::move(func));
-  state_ = nullptr;
 }
 
 template <class T>
index fcec435380a8ad49f010411e6ae12fe95eff50a6..e468eebe6cb5c5edea3df2a619199e33b137184e 100644 (file)
@@ -189,6 +189,8 @@ class Future {
   explicit
   Future(statePtr obj) : state_(obj) {}
 
+  void detach();
+
   void throwIfInvalid() const;
 
   friend class Promise<T>;
index 190d9cd04168725b71146239a11c30157ac239e0..ef6bdb41c4c050261ddee868b5aaa524cb27e151 100644 (file)
@@ -29,9 +29,8 @@ Promise<T>::Promise() : retrieved_(false), state_(new detail::State<T>())
 {}
 
 template <class T>
-Promise<T>::Promise(Promise<T>&& other) :
-retrieved_(other.retrieved_), state_(other.state_) {
-  other.state_ = nullptr;
+Promise<T>::Promise(Promise<T>&& other) : state_(nullptr) {
+  *this = std::move(other);
 }
 
 template <class T>
@@ -44,6 +43,8 @@ Promise<T>& Promise<T>::operator=(Promise<T>&& other) {
 template <class T>
 void Promise<T>::throwIfFulfilled() {
   if (!state_)
+    throw NoState();
+  if (state_->ready())
     throw PromiseAlreadySatisfied();
 }
 
@@ -55,8 +56,16 @@ void Promise<T>::throwIfRetrieved() {
 
 template <class T>
 Promise<T>::~Promise() {
+  detach();
+}
+
+template <class T>
+void Promise<T>::detach() {
   if (state_) {
-    setException(BrokenPromise());
+    if (!retrieved_)
+      state_->detachFuture();
+    state_->detachPromise();
+    state_ = nullptr;
   }
 }
 
@@ -71,7 +80,6 @@ Future<T> Promise<T>::getFuture() {
 template <class T>
 template <class E>
 void Promise<T>::setException(E const& e) {
-  throwIfFulfilled();
   setException(std::make_exception_ptr<E>(e));
 }
 
@@ -79,20 +87,12 @@ template <class T>
 void Promise<T>::setException(std::exception_ptr const& e) {
   throwIfFulfilled();
   state_->setException(e);
-  if (!retrieved_) {
-    delete state_;
-  }
-  state_ = nullptr;
 }
 
 template <class T>
 void Promise<T>::fulfilTry(Try<T>&& t) {
   throwIfFulfilled();
   state_->fulfil(std::move(t));
-  if (!retrieved_) {
-    delete state_;
-  }
-  state_ = nullptr;
 }
 
 template <class T>
@@ -101,12 +101,7 @@ void Promise<T>::setValue(M&& v) {
   static_assert(!std::is_same<T, void>::value,
                 "Use setValue() instead");
 
-  throwIfFulfilled();
-  state_->fulfil(Try<T>(std::forward<M>(v)));
-  if (!retrieved_) {
-    delete state_;
-  }
-  state_ = nullptr;
+  fulfilTry(Try<T>(std::forward<M>(v)));
 }
 
 template <class T>
@@ -114,47 +109,14 @@ void Promise<T>::setValue() {
   static_assert(std::is_same<T, void>::value,
                 "Use setValue(value) instead");
 
-  throwIfFulfilled();
-  state_->fulfil(Try<void>());
-  if (!retrieved_) {
-    delete state_;
-  }
-  state_ = nullptr;
+  fulfilTry(Try<void>());
 }
 
 template <class T>
 template <class F>
 void Promise<T>::fulfil(F&& func) {
-  fulfilHelper(std::forward<F>(func));
-}
-
-template <class T>
-template <class F>
-typename std::enable_if<
-  std::is_convertible<typename std::result_of<F()>::type, T>::value &&
-  !std::is_same<T, void>::value>::type
-inline Promise<T>::fulfilHelper(F&& func) {
-  throwIfFulfilled();
-  try {
-    setValue(func());
-  } catch (...) {
-    setException(std::current_exception());
-  }
-}
-
-template <class T>
-template <class F>
-typename std::enable_if<
-  std::is_same<typename std::result_of<F()>::type, void>::value &&
-  std::is_same<T, void>::value>::type
-inline Promise<T>::fulfilHelper(F&& func) {
   throwIfFulfilled();
-  try {
-    func();
-    setValue();
-  } catch (...) {
-    setException(std::current_exception());
-  }
+  fulfilTry(makeTryFunction(std::forward<F>(func)));
 }
 
 }}
index c47cc98e95693a55ba89284d3e2185180a767321..62e6f6d83e4fd2287061c5ca9a3305b8e27af045 100644 (file)
@@ -86,18 +86,7 @@ private:
 
   void throwIfFulfilled();
   void throwIfRetrieved();
-
-  template <class F>
-  typename std::enable_if<
-    std::is_convertible<typename std::result_of<F()>::type, T>::value &&
-    !std::is_same<T, void>::value>::type
-  fulfilHelper(F&& func);
-
-  template <class F>
-  typename std::enable_if<
-    std::is_same<typename std::result_of<F()>::type, void>::value &&
-    std::is_same<T, void>::value>::type
-  fulfilHelper(F&& func);
+  void detach();
 };
 
 }}
index bf2a34347afdae3cd2fe031f2c54398f19ca2bfe..39b6a7f2c4b573695c4a083e687260e617099c52 100644 (file)
@@ -17,6 +17,7 @@
 #pragma once
 
 #include <atomic>
+#include <mutex>
 #include <stdexcept>
 #include <vector>
 
@@ -32,7 +33,14 @@ namespace folly { namespace wangle { namespace detail {
 template<typename T>
 class State {
  public:
+  // This must be heap-constructed. There's probably a way to enforce that in
+  // code but since this is just internal detail code and I don't know how
+  // off-hand, I'm punting.
   State() = default;
+  ~State() {
+    assert(calledBack_);
+    assert(detached_ == 2);
+  }
 
   // not copyable
   State(State const&) = delete;
@@ -48,29 +56,32 @@ class State {
 
   template <typename F>
   void setCallback(F func) {
-    if (callback_) {
-      throw std::logic_error("setCallback called twice");
-    }
+    {
+      std::lock_guard<std::mutex> lock(mutex_);
 
-    callback_ = std::move(func);
+      if (callback_) {
+        throw std::logic_error("setCallback called twice");
+      }
 
-    if (shouldContinue_.test_and_set()) {
-      callback_(std::move(*value_));
-      delete this;
+      callback_ = std::move(func);
     }
+
+    maybeCallback();
   }
 
   void fulfil(Try<T>&& t) {
-    if (value_.hasValue()) {
-      throw std::logic_error("fulfil called twice");
-    }
+    {
+      std::lock_guard<std::mutex> lock(mutex_);
 
-    value_ = std::move(t);
+      if (ready()) {
+        throw std::logic_error("fulfil called twice");
+      }
 
-    if (shouldContinue_.test_and_set()) {
-      callback_(std::move(*value_));
-      delete this;
+      value_ = std::move(t);
+      assert(ready());
     }
+
+    maybeCallback();
   }
 
   void setException(std::exception_ptr const& e) {
@@ -93,10 +104,57 @@ class State {
     }
   }
 
+  // Called by a destructing Future
+  void detachFuture() {
+    if (!callback_) {
+      setCallback([](Try<T>&&) {});
+    }
+    detachOne();
+  }
+
+  // Called by a destructing Promise
+  void detachPromise() {
+    if (!ready()) {
+      setException(BrokenPromise());
+    }
+    detachOne();
+  }
+
  private:
-  std::atomic_flag shouldContinue_ = ATOMIC_FLAG_INIT;
+  void maybeCallback() {
+    std::lock_guard<std::mutex> lock(mutex_);
+    if (value_ && callback_) {
+      // TODO we should probably try/catch here
+      callback_(std::move(*value_));
+      calledBack_ = true;
+    }
+  }
+
+  void detachOne() {
+    bool shouldDelete;
+    {
+      std::lock_guard<std::mutex> lock(mutex_);
+      detached_++;
+      assert(detached_ == 1 || detached_ == 2);
+      shouldDelete = (detached_ == 2);
+    }
+
+    if (shouldDelete) {
+      // we should have already executed the callback with the value
+      assert(calledBack_);
+      delete this;
+    }
+  }
+
   folly::Optional<Try<T>> value_;
   std::function<void(Try<T>&&)> callback_;
+  bool calledBack_ = false;
+  unsigned char detached_ = 0;
+
+  // this lock isn't meant to protect all accesses to members, only the ones
+  // that need to be threadsafe: the act of setting value_ and callback_, and
+  // seeing if they are set and whether we should then continue.
+  std::mutex mutex_;
 };
 
 template <typename... Ts>
index 97dd82a836aef3849e126b29f62cdf79c0db6a63..cbf76a152bca300460637b2eedee81788bf1df42 100644 (file)
@@ -276,20 +276,21 @@ TEST(Promise, fulfil) {
 
 TEST(Future, finish) {
   auto x = std::make_shared<int>(0);
-  Promise<int> p;
-  auto f = p.getFuture().then([x](Try<int>&& t) { *x = t.value(); });
-
-  // The callback hasn't executed
-  EXPECT_EQ(0, *x);
+  {
+    Promise<int> p;
+    auto f = p.getFuture().then([x](Try<int>&& t) { *x = t.value(); });
 
-  // The callback has a reference to x
-  EXPECT_EQ(2, x.use_count());
+    // The callback hasn't executed
+    EXPECT_EQ(0, *x);
 
-  p.setValue(42);
+    // The callback has a reference to x
+    EXPECT_EQ(2, x.use_count());
 
-  // the callback has executed
-  EXPECT_EQ(42, *x);
+    p.setValue(42);
 
+    // the callback has executed
+    EXPECT_EQ(42, *x);
+  }
   // the callback has been destructed
   // and has released its reference to x
   EXPECT_EQ(1, x.use_count());