Bug 1200922: Add the ability to shut down a thread asynchronously. r=froydnj
authorKyle Huey <khuey@kylehuey.com>
Mon, 14 Sep 2015 18:24:43 -0700
changeset 295111 6bb087e3ec0821954012f8581c2af4d4930c8a59
parent 295110 6b165bd0e7864e8d1c998b403f75509f323c57bb
child 295112 cbea1d09d4135c87a06717b15a05c3b2dfdb9deb
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfroydnj
bugs1200922
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1200922: Add the ability to shut down a thread asynchronously. r=froydnj
xpcom/tests/gtest/TestThreads.cpp
xpcom/threads/LazyIdleThread.cpp
xpcom/threads/nsIThread.idl
xpcom/threads/nsThread.cpp
xpcom/threads/nsThread.h
xpcom/threads/nsThreadPool.cpp
--- a/xpcom/tests/gtest/TestThreads.cpp
+++ b/xpcom/tests/gtest/TestThreads.cpp
@@ -6,16 +6,17 @@
 
 #include "nsThreadUtils.h"
 #include <stdio.h>
 #include <stdlib.h>
 #include "nspr.h"
 #include "nsCOMPtr.h"
 #include "nsIServiceManager.h"
 #include "nsXPCOM.h"
+#include "mozilla/Monitor.h"
 #include "gtest/gtest.h"
 
 class nsRunner final : public nsIRunnable {
   ~nsRunner() {}
 public:
     NS_DECL_THREADSAFE_ISUPPORTS
 
     NS_IMETHOD Run() override {
@@ -119,16 +120,126 @@ TEST(Threads, Stress)
         for (k = threads-1; k >= 0; k--) {
             array[k]->Shutdown();
             NS_RELEASE(array[k]);    
         }
         delete [] array;
     }
 }
 
+mozilla::Monitor* gAsyncShutdownReadyMonitor;
+mozilla::Monitor* gBeginAsyncShutdownMonitor;
+
+class AsyncShutdownPreparer : public nsIRunnable {
+public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+
+    NS_IMETHOD Run() override {
+        EXPECT_FALSE(mWasRun);
+        mWasRun = true;
+
+        mozilla::MonitorAutoLock lock(*gAsyncShutdownReadyMonitor);
+        lock.Notify();
+
+        return NS_OK;
+    }
+
+    explicit AsyncShutdownPreparer() : mWasRun(false) {}
+
+private:
+    virtual ~AsyncShutdownPreparer() {
+        EXPECT_TRUE(mWasRun);
+    }
+
+protected:
+    bool mWasRun;
+};
+
+NS_IMPL_ISUPPORTS(AsyncShutdownPreparer, nsIRunnable)
+
+class AsyncShutdownWaiter : public nsIRunnable {
+public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+
+    NS_IMETHOD Run() override {
+        EXPECT_FALSE(mWasRun);
+        mWasRun = true;
+
+        nsCOMPtr<nsIThread> t;
+        nsresult rv;
+
+        {
+          mozilla::MonitorAutoLock lock(*gBeginAsyncShutdownMonitor);
+
+          rv = NS_NewThread(getter_AddRefs(t), new AsyncShutdownPreparer());
+          EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+          lock.Wait();
+        }
+
+        rv = t->AsyncShutdown();
+        EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+        return NS_OK;
+    }
+
+    explicit AsyncShutdownWaiter() : mWasRun(false) {}
+
+private:
+    virtual ~AsyncShutdownWaiter() {
+        EXPECT_TRUE(mWasRun);
+    }
+
+protected:
+    bool mWasRun;
+};
+
+NS_IMPL_ISUPPORTS(AsyncShutdownWaiter, nsIRunnable)
+
+class SameThreadSentinel : public nsIRunnable {
+public:
+    NS_DECL_ISUPPORTS
+
+    NS_IMETHOD Run() override {
+        mozilla::MonitorAutoLock lock(*gBeginAsyncShutdownMonitor);
+        lock.Notify();
+        return NS_OK;
+    }
+
+private:
+    virtual ~SameThreadSentinel() {}
+};
+
+NS_IMPL_ISUPPORTS(SameThreadSentinel, nsIRunnable)
+
+TEST(Threads, AsyncShutdown)
+{
+  gAsyncShutdownReadyMonitor = new mozilla::Monitor("gAsyncShutdownReady");
+  gBeginAsyncShutdownMonitor = new mozilla::Monitor("gBeginAsyncShutdown");
+
+  nsCOMPtr<nsIThread> t;
+  nsresult rv;
+
+  {
+    mozilla::MonitorAutoLock lock(*gAsyncShutdownReadyMonitor);
+
+    rv = NS_NewThread(getter_AddRefs(t), new AsyncShutdownWaiter());
+    EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+    lock.Wait();
+  }
+
+  NS_DispatchToCurrentThread(new SameThreadSentinel());
+  rv = t->Shutdown();
+  EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+  delete gAsyncShutdownReadyMonitor;
+  delete gBeginAsyncShutdownMonitor;
+}
+
 static void threadProc(void *arg)
 {
     // printf("   running thread %d\n", (int) arg);
     PR_Sleep(1);
     EXPECT_EQ(PR_JOINABLE_THREAD, PR_GetThreadState(PR_GetCurrentThread()));
 }
 
 TEST(Threads, StressNSPR)
--- a/xpcom/threads/LazyIdleThread.cpp
+++ b/xpcom/threads/LazyIdleThread.cpp
@@ -452,16 +452,23 @@ LazyIdleThread::GetPRThread(PRThread** a
     return mThread->GetPRThread(aPRThread);
   }
 
   *aPRThread = nullptr;
   return NS_ERROR_NOT_AVAILABLE;
 }
 
 NS_IMETHODIMP
+LazyIdleThread::AsyncShutdown()
+{
+  ASSERT_OWNING_THREAD();
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
 LazyIdleThread::Shutdown()
 {
   ASSERT_OWNING_THREAD();
 
   mShutdown = true;
 
   nsresult rv = ShutdownThread();
   MOZ_ASSERT(!mThread, "Should have destroyed this by now!");
--- a/xpcom/threads/nsIThread.idl
+++ b/xpcom/threads/nsIThread.idl
@@ -12,17 +12,17 @@
  * This interface provides a high-level abstraction for an operating system
  * thread.
  *
  * Threads have a built-in event queue, and a thread is an event target that
  * can receive nsIRunnable objects (events) to be processed on the thread.
  *
  * See nsIThreadManager for the API used to create and locate threads.
  */
-[scriptable, uuid(9c889946-a73a-4af3-ae9a-ea64f7d4e3ca)]
+[scriptable, uuid(594feb13-6164-4054-b5a1-ad62e10ea15d)]
 interface nsIThread : nsIEventTarget
 {
   /**
    * @returns
    *   The NSPR thread object corresponding to this nsIThread.
    */
   [noscript] readonly attribute PRThread PRThread;
 
@@ -77,9 +77,31 @@ interface nsIThread : nsIEventTarget
    * @returns
    *   A boolean value that if "true" indicates that an event was processed.
    *
    * @throws NS_ERROR_UNEXPECTED
    *   Indicates that this method was erroneously called when this thread was
    *   not the current thread.
    */
   boolean processNextEvent(in boolean mayWait);
+
+  /**
+   * Shutdown the thread asynchronously.  This method immediately prevents
+   * further dispatch of events to the thread, and it causes any pending events
+   * to run to completion before this thread joins with the current thread.
+   *
+   * UNLIKE shutdown() this does not process events on the current thread.
+   * Instead it merely ensures that the current thread continues running until
+   * this thread has shut down.
+   *
+   * This method MAY NOT be executed from the thread itself.  Instead, it is
+   * meant to be executed from another thread (usually the thread that created
+   * this thread or the main application thread).  When this function returns,
+   * the thread will continue running until it exhausts its event queue.
+   *
+   * @throws NS_ERROR_UNEXPECTED
+   *   Indicates that this method was erroneously called when this thread was
+   *   the current thread, that this thread was not created with a call to
+   *   nsIThreadManager::NewThread, or if this method was called more than once
+   *   on the thread object.
+   */
+  void asyncShutdown();
 };
--- a/xpcom/threads/nsThread.cpp
+++ b/xpcom/threads/nsThread.cpp
@@ -16,16 +16,17 @@
 #endif
 
 #include "mozilla/ReentrantMonitor.h"
 #include "nsMemoryPressure.h"
 #include "nsThreadManager.h"
 #include "nsIClassInfoImpl.h"
 #include "nsAutoPtr.h"
 #include "nsCOMPtr.h"
+#include "nsQueryObject.h"
 #include "pratom.h"
 #include "mozilla/CycleCollectedJSRuntime.h"
 #include "mozilla/Logging.h"
 #include "nsIObserverService.h"
 #if !defined(MOZILLA_XPCOMRT_API)
 #include "mozilla/HangMonitor.h"
 #include "mozilla/IOInterposer.h"
 #include "mozilla/ipc/MessageChannel.h"
@@ -233,38 +234,53 @@ private:
   ReentrantMonitor mMon;
   bool mInitialized;
 };
 
 //-----------------------------------------------------------------------------
 
 struct nsThreadShutdownContext
 {
+  // NB: This will be the last reference.
+  nsRefPtr<nsThread> terminatingThread;
   nsThread* joiningThread;
-  bool      shutdownAck;
+  bool      awaitingShutdownAck;
 };
 
 // This event is responsible for notifying nsThread::Shutdown that it is time
-// to call PR_JoinThread.
-class nsThreadShutdownAckEvent : public nsRunnable
+// to call PR_JoinThread. It implements nsICancelableRunnable so that it can
+// run on a DOM Worker thread (where all events must implement
+// nsICancelableRunnable.)
+class nsThreadShutdownAckEvent : public nsRunnable,
+                                 public nsICancelableRunnable
 {
 public:
   explicit nsThreadShutdownAckEvent(nsThreadShutdownContext* aCtx)
     : mShutdownContext(aCtx)
   {
   }
-  NS_IMETHOD Run()
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_IMETHOD Run() override
   {
-    mShutdownContext->shutdownAck = true;
+    mShutdownContext->terminatingThread->ShutdownComplete(mShutdownContext);
     return NS_OK;
   }
+  NS_IMETHOD Cancel() override
+  {
+    return Run();
+  }
 private:
+  virtual ~nsThreadShutdownAckEvent() { }
+
   nsThreadShutdownContext* mShutdownContext;
 };
 
+NS_IMPL_ISUPPORTS_INHERITED(nsThreadShutdownAckEvent, nsRunnable,
+                            nsICancelableRunnable)
+
 // This event is responsible for setting mShutdownContext
 class nsThreadShutdownEvent : public nsRunnable
 {
 public:
   nsThreadShutdownEvent(nsThread* aThr, nsThreadShutdownContext* aCtx)
     : mThread(aThr)
     , mShutdownContext(aCtx)
   {
@@ -364,18 +380,25 @@ nsThread::ThreadFunc(void* aArg)
 
     BackgroundChild::CloseForCurrentThread();
 #endif // defined(MOZILLA_XPCOMRT_API)
 
     // Do NS_ProcessPendingEvents but with special handling to set
     // mEventsAreDoomed atomically with the removal of the last event. The key
     // invariant here is that we will never permit PutEvent to succeed if the
     // event would be left in the queue after our final call to
-    // NS_ProcessPendingEvents.
+    // NS_ProcessPendingEvents. We also have to keep processing events as long
+    // as we have outstanding mRequestedShutdownContexts.
     while (true) {
+      // Check and see if we're waiting on any threads.
+      while (self->mRequestedShutdownContexts.Length()) {
+        // We can't stop accepting events just yet.  Block and check again.
+        NS_ProcessNextEvent(self, true);
+      }
+
       {
         MutexAutoLock lock(self->mLock);
         if (!self->mEvents->HasPendingEvent()) {
           // No events in the queue, so we will stop now. Don't let any more
           // events be added, since they won't be processed. It is critical
           // that no PutEvent can occur between testing that the event queue is
           // empty and setting mEventsAreDoomed!
           self->mEventsAreDoomed = true;
@@ -389,17 +412,18 @@ nsThread::ThreadFunc(void* aArg)
 #if !defined(MOZILLA_XPCOMRT_API)
   mozilla::IOInterposer::UnregisterCurrentThread();
 #endif // !defined(MOZILLA_XPCOMRT_API)
 
   // Inform the threadmanager that this thread is going away
   nsThreadManager::get()->UnregisterCurrentThread(self);
 
   // Dispatch shutdown ACK
-  event = new nsThreadShutdownAckEvent(self->mShutdownContext);
+  MOZ_ASSERT(self->mShutdownContext->terminatingThread == self);
+  event = do_QueryObject(new nsThreadShutdownAckEvent(self->mShutdownContext));
   self->mShutdownContext->joiningThread->Dispatch(event, NS_DISPATCH_NORMAL);
 
   // Release any observer of the thread here.
   self->SetObserver(nullptr);
 
 #ifdef MOZ_TASK_TRACER
   FreeTraceInfo();
 #endif
@@ -626,58 +650,82 @@ nsThread::IsOnCurrentThread(bool* aResul
 NS_IMETHODIMP
 nsThread::GetPRThread(PRThread** aResult)
 {
   *aResult = mThread;
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsThread::Shutdown()
+nsThread::AsyncShutdown()
 {
-  LOG(("THRD(%p) shutdown\n", this));
+  LOG(("THRD(%p) async shutdown\n", this));
 
   // XXX If we make this warn, then we hit that warning at xpcom shutdown while
   //     shutting down a thread in a thread pool.  That happens b/c the thread
   //     in the thread pool is already shutdown by the thread manager.
   if (!mThread) {
     return NS_OK;
   }
 
+  return !!ShutdownInternal(/* aSync = */ false) ? NS_OK : NS_ERROR_UNEXPECTED;
+}
+
+nsThreadShutdownContext*
+nsThread::ShutdownInternal(bool aSync)
+{
+  MOZ_ASSERT(mThread);
+
   if (NS_WARN_IF(mThread == PR_GetCurrentThread())) {
-    return NS_ERROR_UNEXPECTED;
+    return nullptr;
   }
 
   // Prevent multiple calls to this method
   {
     MutexAutoLock lock(mLock);
     if (!mShutdownRequired) {
-      return NS_ERROR_UNEXPECTED;
+      return nullptr;
     }
     mShutdownRequired = false;
   }
 
-  nsThreadShutdownContext context;
-  context.joiningThread = nsThreadManager::get()->GetCurrentThread();
-  context.shutdownAck = false;
+  nsThread* currentThread = nsThreadManager::get()->GetCurrentThread();
+  MOZ_ASSERT(currentThread);
+
+  nsAutoPtr<nsThreadShutdownContext>& context =
+    *currentThread->mRequestedShutdownContexts.AppendElement();
+  context = new nsThreadShutdownContext();
+
+  context->terminatingThread = this;
+  context->joiningThread = currentThread;
+  context->awaitingShutdownAck = aSync;
 
   // Set mShutdownContext and wake up the thread in case it is waiting for
   // events to process.
-  nsCOMPtr<nsIRunnable> event = new nsThreadShutdownEvent(this, &context);
+  nsCOMPtr<nsIRunnable> event = new nsThreadShutdownEvent(this, context);
   // XXXroc What if posting the event fails due to OOM?
   PutEvent(event.forget(), nullptr);
 
   // We could still end up with other events being added after the shutdown
   // task, but that's okay because we process pending events in ThreadFunc
   // after setting mShutdownContext just before exiting.
+  return context;
+}
 
-  // Process events on the current thread until we receive a shutdown ACK.
-  // Allows waiting; ensure no locks are held that would deadlock us!
-  while (!context.shutdownAck) {
-    NS_ProcessNextEvent(context.joiningThread, true);
+void
+nsThread::ShutdownComplete(nsThreadShutdownContext* aContext)
+{
+  MOZ_ASSERT(mThread);
+  MOZ_ASSERT(aContext->terminatingThread == this);
+
+  if (aContext->awaitingShutdownAck) {
+    // We're in a synchronous shutdown, so tell whatever is up the stack that
+    // we're done and unwind the stack so it can call us again.
+    aContext->awaitingShutdownAck = false;
+    return;
   }
 
   // Now, it should be safe to join without fear of dead-locking.
 
   PR_JoinThread(mThread);
   mThread = nullptr;
 
   // We hold strong references to our event observers, and once the thread is
@@ -687,16 +735,44 @@ nsThread::Shutdown()
 
 #ifdef DEBUG
   {
     MutexAutoLock lock(mLock);
     MOZ_ASSERT(!mObserver, "Should have been cleared at shutdown!");
   }
 #endif
 
+  // Delete aContext.
+  MOZ_ALWAYS_TRUE(
+    aContext->joiningThread->mRequestedShutdownContexts.RemoveElement(aContext));
+}
+
+NS_IMETHODIMP
+nsThread::Shutdown()
+{
+  LOG(("THRD(%p) sync shutdown\n", this));
+
+  // XXX If we make this warn, then we hit that warning at xpcom shutdown while
+  //     shutting down a thread in a thread pool.  That happens b/c the thread
+  //     in the thread pool is already shutdown by the thread manager.
+  if (!mThread) {
+    return NS_OK;
+  }
+
+  nsThreadShutdownContext* context = ShutdownInternal(/* aSync = */ true);
+  NS_ENSURE_TRUE(context, NS_ERROR_UNEXPECTED);
+
+  // Process events on the current thread until we receive a shutdown ACK.
+  // Allows waiting; ensure no locks are held that would deadlock us!
+  while (context->awaitingShutdownAck) {
+    NS_ProcessNextEvent(context->joiningThread, true);
+  }
+
+  ShutdownComplete(context);
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsThread::HasPendingEvents(bool* aResult)
 {
   if (NS_WARN_IF(PR_GetCurrentThread() != mThread)) {
     return NS_ERROR_NOT_SAME_THREAD;
--- a/xpcom/threads/nsThread.h
+++ b/xpcom/threads/nsThread.h
@@ -72,16 +72,18 @@ public:
   }
 
   void
   SetScriptObserver(mozilla::CycleCollectedJSRuntime* aScriptObserver);
 
   uint32_t
   RecursionDepth() const;
 
+  void ShutdownComplete(struct nsThreadShutdownContext* aContext);
+
 protected:
   class nsChainedEventQueue;
 
   class nsNestedEventTarget;
   friend class nsNestedEventTarget;
 
   friend class nsThreadShutdownEvent;
 
@@ -108,16 +110,18 @@ protected:
     return mEvents->GetEvent(aMayWait, aEvent);
   }
   nsresult PutEvent(nsIRunnable* aEvent, nsNestedEventTarget* aTarget);
   nsresult PutEvent(already_AddRefed<nsIRunnable>&& aEvent, nsNestedEventTarget* aTarget);
 
   nsresult DispatchInternal(already_AddRefed<nsIRunnable>&& aEvent, uint32_t aFlags,
                             nsNestedEventTarget* aTarget);
 
+  struct nsThreadShutdownContext* ShutdownInternal(bool aSync);
+
   // Wrapper for nsEventQueue that supports chaining.
   class nsChainedEventQueue
   {
   public:
     nsChainedEventQueue()
       : mNext(nullptr)
     {
     }
@@ -188,17 +192,20 @@ protected:
   nsChainedEventQueue* mEvents;  // never null
   nsChainedEventQueue  mEventsRoot;
 
   int32_t   mPriority;
   PRThread* mThread;
   uint32_t  mNestedEventLoopDepth;
   uint32_t  mStackSize;
 
+  // The shutdown context for ourselves.
   struct nsThreadShutdownContext* mShutdownContext;
+  // The shutdown contexts for any other threads we've asked to shut down.
+  nsTArray<nsAutoPtr<struct nsThreadShutdownContext>> mRequestedShutdownContexts;
 
   bool mShutdownRequired;
   // Set to true when events posted to this thread will never run.
   bool mEventsAreDoomed;
   MainThreadFlag mIsMainThread;
 };
 
 //-----------------------------------------------------------------------------
--- a/xpcom/threads/nsThreadPool.cpp
+++ b/xpcom/threads/nsThreadPool.cpp
@@ -121,25 +121,19 @@ nsThreadPool::PutEvent(already_AddRefed<
     if (mThreads.Count() < (int32_t)mThreadLimit) {
       mThreads.AppendObject(thread);
     } else {
       killThread = true;  // okay, we don't need this thread anymore
     }
   }
   LOG(("THRD-P(%p) put [%p kill=%d]\n", this, thread.get(), killThread));
   if (killThread) {
-    // Pending events are processed on the current thread during
-    // nsIThread::Shutdown() execution, so if nsThreadPool::Dispatch() is called
-    // under caller's lock then deadlock could occur. This happens e.g. in case
-    // of nsStreamCopier. To prevent this situation, dispatch a shutdown event
-    // to the current thread instead of calling nsIThread::Shutdown() directly.
-
-    nsCOMPtr<nsIRunnable> r = NS_NewRunnableMethod(thread,
-                                                   &nsIThread::Shutdown);
-    NS_DispatchToCurrentThread(r);
+    // We never dispatched any events to the thread, so we can shut it down
+    // asynchronously without worrying about anything.
+    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(thread->AsyncShutdown()));
   } else {
     thread->Dispatch(this, NS_DISPATCH_NORMAL);
   }
 
   return NS_OK;
 }
 
 void