Introduce RunLoop::Type::NESTABLE_TASKS_ALLOWED to replace MessageLoop::ScopedNestableTaskAllower.
(as well as MessageLoop::SetNestableTasksAllowed())
Surveying usage: the scoped object is always instantiated right before
RunLoop().Run(). The intent is really to allow nestable tasks in that
RunLoop so it's better to explicitly label that RunLoop as such and it
allows us to break the last dependency that forced some RunLoop users
to use MessageLoop APIs.
There's also the odd case of allowing nestable tasks for loops that are
reentrant from a native task (without going through RunLoop), these
are the minority but will have to be handled (after cleaning up the
majority of cases that are RunLoop induced).
As highlighted by robliao@ in https://chromium-review.googlesource.com/c/600517
(which was merged in this CL).
[email protected]
Bug: 750779
Change-Id: I43d122c93ec903cff3a6fe7b77ec461ea0656448
Reviewed-on: https://chromium-review.googlesource.com/594713
Commit-Queue: Gabriel Charette <[email protected]>
Reviewed-by: Robert Liao <[email protected]>
Reviewed-by: danakj <[email protected]>
Cr-Commit-Position: refs/heads/master@{#492263}
diff --git a/base/message_loop/message_loop.cc b/base/message_loop/message_loop.cc
index b9cae6c..19e47f2 100644
--- a/base/message_loop/message_loop.cc
+++ b/base/message_loop/message_loop.cc
@@ -236,14 +236,14 @@
CHECK(RunLoop::IsNestingAllowedOnCurrentThread());
// Kick the native pump just in case we enter a OS-driven nested message
- // loop.
+ // loop that does not go through RunLoop::Run().
pump_->ScheduleWork();
}
nestable_tasks_allowed_ = allowed;
}
bool MessageLoop::NestableTasksAllowed() const {
- return nestable_tasks_allowed_;
+ return nestable_tasks_allowed_ || run_loop_client_->ProcessingTasksAllowed();
}
// TODO(gab): Migrate TaskObservers to RunLoop as part of separating concerns
@@ -355,6 +355,13 @@
pump_->Quit();
}
+void MessageLoop::EnsureWorkScheduled() {
+ DCHECK_EQ(this, current());
+ ReloadWorkQueue();
+ if (!work_queue_.empty())
+ pump_->ScheduleWork();
+}
+
void MessageLoop::SetThreadTaskRunnerHandle() {
DCHECK_EQ(this, current());
// Clear the previous thread task runner first, because only one can exist at
@@ -386,7 +393,7 @@
}
void MessageLoop::RunTask(PendingTask* pending_task) {
- DCHECK(nestable_tasks_allowed_);
+ DCHECK(NestableTasksAllowed());
current_pending_task_ = pending_task;
#if defined(OS_WIN)
@@ -491,7 +498,7 @@
}
bool MessageLoop::DoWork() {
- if (!nestable_tasks_allowed_) {
+ if (!NestableTasksAllowed()) {
// Task can't be executed right now.
return false;
}
@@ -529,7 +536,7 @@
}
bool MessageLoop::DoDelayedWork(TimeTicks* next_delayed_work_time) {
- if (!nestable_tasks_allowed_ ||
+ if (!NestableTasksAllowed() ||
!SweepDelayedWorkQueueAndReturnTrueIfStillHasWork()) {
recent_time_ = *next_delayed_work_time = TimeTicks();
return false;
diff --git a/base/message_loop/message_loop.h b/base/message_loop/message_loop.h
index 5d076f0..73c47d1 100644
--- a/base/message_loop/message_loop.h
+++ b/base/message_loop/message_loop.h
@@ -225,10 +225,20 @@
// - With NestableTasksAllowed set to true, the task #2 will run right away.
// Otherwise, it will get executed right after task #1 completes at "thread
// message loop level".
+ //
+ // DEPRECATED: Use RunLoop::Type on the relevant RunLoop instead of these
+ // methods.
+ // TODO(gab): Migrate usage and delete these methods.
void SetNestableTasksAllowed(bool allowed);
bool NestableTasksAllowed() const;
// Enables nestable tasks on |loop| while in scope.
+ // DEPRECATED: This should not be used when the nested loop is driven by
+ // RunLoop (use RunLoop::Type::KNestableTasksAllowed instead). It can however
+ // still be useful in a few scenarios where re-entrancy is caused by a native
+ // message loop.
+ // TODO(gab): Remove usage of this class alongside RunLoop and rename it to
+ // ScopedApplicationTasksAllowedInNativeNestedLoop(?).
class ScopedNestableTaskAllower {
public:
explicit ScopedNestableTaskAllower(MessageLoop* loop)
@@ -334,6 +344,7 @@
// RunLoop::Delegate:
void Run() override;
void Quit() override;
+ void EnsureWorkScheduled() override;
// Called to process any delayed non-nestable tasks.
bool ProcessNextDelayedNonNestableTask();
@@ -401,7 +412,9 @@
ObserverList<DestructionObserver> destruction_observers_;
// A recursion block that prevents accidentally running additional tasks when
- // insider a (accidentally induced?) nested message pump.
+ // insider a (accidentally induced?) nested message pump. Deprecated in favor
+ // of run_loop_client_->ProcessingTasksAllowed(), equivalent until then (both
+ // need to be checked in conditionals).
bool nestable_tasks_allowed_;
// pump_factory_.Run() is called to create a message pump for this loop
diff --git a/base/run_loop.cc b/base/run_loop.cc
index 072684f6..b11e71364 100644
--- a/base/run_loop.cc
+++ b/base/run_loop.cc
@@ -60,6 +60,14 @@
return outer_->active_run_loops_.size() > 1;
}
+bool RunLoop::Delegate::Client::ProcessingTasksAllowed() const {
+ DCHECK_CALLED_ON_VALID_THREAD(outer_->bound_thread_checker_);
+ DCHECK(outer_->bound_);
+ DCHECK(!outer_->active_run_loops_.empty());
+ return outer_->active_run_loops_.size() == 1U ||
+ outer_->active_run_loops_.top()->type_ == Type::kNestableTasksAllowed;
+}
+
RunLoop::Delegate::Client::Client(Delegate* outer) : outer_(outer) {}
// static
@@ -77,13 +85,17 @@
return &delegate->client_interface_;
}
-RunLoop::RunLoop()
+RunLoop::RunLoop(Type type)
: delegate_(tls_delegate.Get().Get()),
+ type_(type),
origin_task_runner_(ThreadTaskRunnerHandle::Get()),
weak_factory_(this) {
DCHECK(delegate_) << "A RunLoop::Delegate must be bound to this thread prior "
"to using RunLoop.";
DCHECK(origin_task_runner_);
+
+ DCHECK(IsNestingAllowedOnCurrentThread() ||
+ type_ != Type::kNestableTasksAllowed);
}
RunLoop::~RunLoop() {
@@ -253,6 +265,8 @@
CHECK(delegate_->allow_nesting_);
for (auto& observer : delegate_->nesting_observers_)
observer.OnBeginNestedRunLoop();
+ if (type_ == Type::kNestableTasksAllowed)
+ delegate_->EnsureWorkScheduled();
}
running_ = true;
diff --git a/base/run_loop.h b/base/run_loop.h
index 82945d92..8c6e649 100644
--- a/base/run_loop.h
+++ b/base/run_loop.h
@@ -38,7 +38,36 @@
// a nested RunLoop but please do not use nested loops in production code!
class BASE_EXPORT RunLoop {
public:
- RunLoop();
+ // The type of RunLoop: a kDefault RunLoop at the top-level (non-nested) will
+ // process system and application tasks assigned to its Delegate. When nested
+ // however a kDefault RunLoop will only process system tasks while a
+ // kNestableTasksAllowed RunLoop will continue to process application tasks
+ // even if nested.
+ //
+ // This is relevant in the case of recursive RunLoops. Some unwanted run loops
+ // may occur when using common controls or printer functions. By default,
+ // recursive task processing is disabled.
+ //
+ // In general, nestable RunLoops are to be avoided. They are dangerous and
+ // difficult to get right, so please use with extreme caution. To further
+ // protect this: kNestableTasksAllowed RunLoops are only allowed on threads
+ // where IsNestingAllowedOnCurrentThread().
+ //
+ // A specific example where this makes a difference is:
+ // - The thread is running a RunLoop.
+ // - It receives a task #1 and executes it.
+ // - The task #1 implicitly starts a RunLoop, like a MessageBox in the unit
+ // test. This can also be StartDoc or GetSaveFileName.
+ // - The thread receives a task #2 before or while in this second RunLoop.
+ // - With a kNestableTasksAllowed RunLoop, the task #2 will run right away.
+ // Otherwise, it will get executed right after task #1 completes in the main
+ // RunLoop.
+ enum class Type {
+ kDefault,
+ kNestableTasksAllowed,
+ };
+
+ RunLoop(Type type = Type::kDefault);
~RunLoop();
// Run the current RunLoop::Delegate. This blocks until Quit is called. Before
@@ -71,7 +100,7 @@
// RunLoop has already finished running has no effect.
//
// WARNING: You must NEVER assume that a call to Quit() or QuitWhenIdle() will
- // terminate the targetted message loop. If a nested run loop continues
+ // terminate the targetted message loop. If a nested RunLoop continues
// running, the target may NEVER terminate. It is very easy to livelock (run
// forever) in such a case.
void Quit();
@@ -100,7 +129,7 @@
// Safe to call before RegisterDelegateForCurrentThread().
static bool IsNestedOnCurrentThread();
- // A NestingObserver is notified when a nested run loop begins. The observers
+ // A NestingObserver is notified when a nested RunLoop begins. The observers
// are notified before the current thread's RunLoop::Delegate::Run() is
// invoked and nested work begins.
class BASE_EXPORT NestingObserver {
@@ -144,10 +173,14 @@
// Returns true if this |outer_| is currently in nested runs. This is a
// shortcut for RunLoop::IsNestedOnCurrentThread() for the owner of this
// interface.
- // TODO(gab): consider getting rid of this and the Client class altogether
- // when it's the only method left on Client. http://crbug.com/703346.
bool IsNested() const;
+ // Returns true if the Delegate is allowed to process application tasks.
+ // This typically returns true except in nested RunLoops outside the scope
+ // of a ScopedNestableTaskAllowed as, by default, nested RunLoops are only
+ // meant to process system events.
+ bool ProcessingTasksAllowed() const;
+
private:
// Only a Delegate can instantiate a Delegate::Client.
friend class Delegate;
@@ -166,12 +199,24 @@
// eventual matching Quit() call. Upon receiving a Quit() call it should
// return from the Run() call as soon as possible without executing
// remaining tasks/messages. Run() calls can nest in which case each Quit()
- // call should result in the topmost active Run() call returning. The
- // only other trigger for Run() to return is Client::ShouldQuitWhenIdle()
- // which the Delegate should probe before sleeping when it becomes idle.
+ // call should result in the topmost active Run() call returning. The only
+ // other trigger for Run() to return is Client::ShouldQuitWhenIdle() which
+ // the Delegate should probe before sleeping when it becomes idle. Run()
+ // implementations should also check Client::ProcessingTasksAllowed() before
+ // processing assigned application tasks (they should only process system
+ // tasks otherwise).
virtual void Run() = 0;
virtual void Quit() = 0;
+ // Invoked right before a RunLoop enters a nested Run() call on this
+ // Delegate iff this RunLoop is of type kNestableTasksAllowed. The Delegate
+ // should ensure that the upcoming Run() call will result in processing
+ // application tasks queued ahead of it without further probing. e.g.
+ // message pumps on some platforms, like Mac, need an explicit request to
+ // process application tasks when nested, otherwise they'll only wait for
+ // system messages.
+ virtual void EnsureWorkScheduled() = 0;
+
// A vector-based stack is more memory efficient than the default
// deque-based stack as the active RunLoop stack isn't expected to ever
// have more than a few entries.
@@ -230,6 +275,8 @@
// during Run(), ref. |sequence_checker_| below).
Delegate* delegate_;
+ const Type type_;
+
#if DCHECK_IS_ON()
bool run_called_ = false;
#endif
diff --git a/base/run_loop_unittest.cc b/base/run_loop_unittest.cc
index 47896f4..5384eaf 100644
--- a/base/run_loop_unittest.cc
+++ b/base/run_loop_unittest.cc
@@ -18,6 +18,7 @@
#include "base/synchronization/waitable_event.h"
#include "base/test/gtest_util.h"
#include "base/test/scoped_task_environment.h"
+#include "base/test/test_timeouts.h"
#include "base/threading/platform_thread.h"
#include "base/threading/thread.h"
#include "base/threading/thread_checker_impl.h"
@@ -44,7 +45,7 @@
}
void RunNestedLoopTask(int* counter) {
- RunLoop nested_run_loop;
+ RunLoop nested_run_loop(RunLoop::Type::kNestableTasksAllowed);
// This task should quit |nested_run_loop| but not the main RunLoop.
ThreadTaskRunnerHandle::Get()->PostTask(
@@ -54,13 +55,6 @@
ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, BindOnce(&ShouldNotRunTask), TimeDelta::FromDays(1));
- std::unique_ptr<MessageLoop::ScopedNestableTaskAllower> allower;
- if (MessageLoop::current()) {
- // Need to allow nestable tasks in MessageLoop driven environments.
- // TODO(gab): Move nestable task allowance concept to RunLoop.
- allower = base::MakeUnique<MessageLoop::ScopedNestableTaskAllower>(
- MessageLoop::current());
- }
nested_run_loop.Run();
++(*counter);
@@ -134,15 +128,40 @@
run_loop_client_ = RunLoop::RegisterDelegateForCurrentThread(this);
}
+ // Runs |closure| on the TestDelegate thread as part of Run(). Useful to
+ // inject code in an otherwise livelocked Run() state.
+ void RunClosureOnDelegate(OnceClosure closure) {
+ AutoLock auto_lock(closure_lock_);
+ closure_ = std::move(closure);
+ }
+
private:
void Run() override {
+ if (nested_run_allowing_tasks_incoming_) {
+ EXPECT_TRUE(run_loop_client_->IsNested());
+ EXPECT_TRUE(run_loop_client_->ProcessingTasksAllowed());
+ } else if (run_loop_client_->IsNested()) {
+ EXPECT_FALSE(run_loop_client_->ProcessingTasksAllowed());
+ }
+ nested_run_allowing_tasks_incoming_ = false;
+
while (!should_quit_) {
- if (simple_task_runner_->ProcessTask())
+ if (run_loop_client_->ProcessingTasksAllowed() &&
+ simple_task_runner_->ProcessTask()) {
continue;
+ }
if (run_loop_client_->ShouldQuitWhenIdle())
break;
+ {
+ AutoLock auto_lock(closure_lock_);
+ if (!closure_.is_null()) {
+ std::move(closure_).Run();
+ continue;
+ }
+ }
+
PlatformThread::YieldCurrentThread();
}
should_quit_ = false;
@@ -150,12 +169,23 @@
void Quit() override { should_quit_ = true; }
+ void EnsureWorkScheduled() override {
+ nested_run_allowing_tasks_incoming_ = true;
+ }
+
+ // True if the next invocation of Run() is expected to be from a
+ // kNestableTasksAllowed RunLoop.
+ bool nested_run_allowing_tasks_incoming_ = false;
+
scoped_refptr<SimpleSingleThreadTaskRunner> simple_task_runner_ =
MakeRefCounted<SimpleSingleThreadTaskRunner>();
std::unique_ptr<ThreadTaskRunnerHandle> thread_task_runner_handle_;
bool should_quit_ = false;
+ Lock closure_lock_;
+ OnceClosure closure_;
+
RunLoop::Delegate::Client* run_loop_client_ = nullptr;
};
@@ -420,7 +450,7 @@
FROM_HERE, BindOnce([]() {
EXPECT_FALSE(RunLoop::IsNestedOnCurrentThread());
- RunLoop nested_run_loop;
+ RunLoop nested_run_loop(RunLoop::Type::kNestableTasksAllowed);
ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, BindOnce([]() {
@@ -430,13 +460,6 @@
nested_run_loop.QuitClosure());
EXPECT_FALSE(RunLoop::IsNestedOnCurrentThread());
- std::unique_ptr<MessageLoop::ScopedNestableTaskAllower> allower;
- if (MessageLoop::current()) {
- // Need to allow nestable tasks in MessageLoop driven environments.
- // TODO(gab): Move nestable task allowance concept to RunLoop.
- allower = base::MakeUnique<MessageLoop::ScopedNestableTaskAllower>(
- MessageLoop::current());
- }
nested_run_loop.Run();
EXPECT_FALSE(RunLoop::IsNestedOnCurrentThread());
}));
@@ -464,20 +487,13 @@
RunLoop::AddNestingObserverOnCurrentThread(&nesting_observer);
const RepeatingClosure run_nested_loop = Bind([]() {
- RunLoop nested_run_loop;
+ RunLoop nested_run_loop(RunLoop::Type::kNestableTasksAllowed);
ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, BindOnce([]() {
EXPECT_TRUE(RunLoop::IsNestingAllowedOnCurrentThread());
}));
ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE,
nested_run_loop.QuitClosure());
- std::unique_ptr<MessageLoop::ScopedNestableTaskAllower> allower;
- if (MessageLoop::current()) {
- // Need to allow nestable tasks in MessageLoop driven environments.
- // TODO(gab): Move nestable task allowance concept to RunLoop.
- allower = base::MakeUnique<MessageLoop::ScopedNestableTaskAllower>(
- MessageLoop::current());
- }
nested_run_loop.Run();
});
@@ -521,4 +537,66 @@
EXPECT_DCHECK_DEATH({ RunLoop(); });
}
+TEST(RunLoopDelegateTest, NestableTasksDontRunInDefaultNestedLoops) {
+ TestDelegate test_delegate;
+ test_delegate.BindToCurrentThread();
+
+ base::Thread other_thread("test");
+ other_thread.Start();
+
+ RunLoop main_loop;
+ // A nested run loop which isn't kNestableTasksAllowed.
+ RunLoop nested_run_loop(RunLoop::Type::kDefault);
+
+ bool nested_run_loop_ended = false;
+
+ // The first task on the main loop will result in a nested run loop. Since
+ // it's not kNestableTasksAllowed, no further task should be processed until
+ // it's quit.
+ ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ BindOnce([](RunLoop* nested_run_loop) { nested_run_loop->Run(); },
+ Unretained(&nested_run_loop)));
+
+ // Post a task that will fail if it runs inside the nested run loop.
+ ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, BindOnce(
+ [](const bool& nested_run_loop_ended,
+ OnceClosure continuation_callback) {
+ EXPECT_TRUE(nested_run_loop_ended);
+ EXPECT_FALSE(RunLoop::IsNestedOnCurrentThread());
+ std::move(continuation_callback).Run();
+ },
+ ConstRef(nested_run_loop_ended), main_loop.QuitClosure()));
+
+ // Post a task flipping the boolean bit for extra verification right before
+ // quitting |nested_run_loop|.
+ other_thread.task_runner()->PostDelayedTask(
+ FROM_HERE,
+ BindOnce(
+ [](bool* nested_run_loop_ended) {
+ EXPECT_FALSE(*nested_run_loop_ended);
+ *nested_run_loop_ended = true;
+ },
+ Unretained(&nested_run_loop_ended)),
+ TestTimeouts::tiny_timeout());
+ // Post an async delayed task to exit the run loop when idle. This confirms
+ // that (1) the test task only ran in the main loop after the nested loop
+ // exited and (2) the nested run loop actually considers itself idle while
+ // spinning. Note: The quit closure needs to be injected directly on the
+ // delegate as invoking QuitWhenIdle() off-thread results in a thread bounce
+ // which will not processed because of the very logic under test (nestable
+ // tasks don't run in |nested_run_loop|).
+ other_thread.task_runner()->PostDelayedTask(
+ FROM_HERE,
+ BindOnce(
+ [](TestDelegate* test_delegate, OnceClosure injected_closure) {
+ test_delegate->RunClosureOnDelegate(std::move(injected_closure));
+ },
+ Unretained(&test_delegate), nested_run_loop.QuitWhenIdleClosure()),
+ TestTimeouts::tiny_timeout());
+
+ main_loop.Run();
+}
+
} // namespace base