The Chromium Chronicle #1: Task Scheduling Best Practices

The Chrome team is proud to introduce the Chromium Chronicle, a monthly series geared specifically to Chromium developers, developers who build the browser.

The Chromium Chronicle will primarily focus on spreading technical knowledge and best practices to write, build, and test Chrome. Our plan is to feature topics that are relevant and useful to Chromium developers, such as code health, helpful tools, unit testing, accessibility and much more! Each article will be written and edited by Chrome engineers.

We are excited about this new series, and hope you are too! Ready to dive in? Take a look at our first episode below!

Task Scheduling Best Practices

Episode 1: by Gabriel Charette in Montréal, PQ (April, 2019)
Previous episodes

Chrome code that needs in-process asynchronous execution typically posts tasks to sequences. Sequences are chrome-managed "virtual threads" and are preferred to creating your own thread. How does an object know which sequence to post to?

Don't

The old paradigm is to receive a SequencedTaskRunner from the creator:

Foo::Foo(scoped_refptr backend_task_runner)
    : backend_task_runner_(std::move(backend_task_runner)) {}
Do

The preferred paradigm is to create an independent SequencedTaskRunner:

Foo::Foo()
    : backend_task_runner_(
          base::CreateSequencedTaskRunnerWithTraits({
              base::MayBlock(), base::TaskPriority::BEST_EFFORT})) {}

This is easier to read and write as all the information is local and there's no risk of inter-dependency with unrelated tasks.

This paradigm is also better when it comes to testing. Instead of injecting task runners manually, tests can instantiate a controlled task environment to manage Foo's tasks:

class FooTest : public testing::Test {
 public
  (...)
 protected:
  base::test::TaskEnvironment task_environment_;
  Foo foo_;
};

Having TaskEnvironment first in the fixture naturally ensures it manages the task environment throughout Foo's lifetime. The TaskEnvironment will capture Foo's request-on-construction to create a SequencedTaskRunner and will manage its tasks under each FooTest.

To test the result of asynchronous execution, use the RunLoop::Run()+QuitClosure() paradigm:

TEST_F(FooTest, TestAsyncWork) {
  RunLoop run_loop;
  foo_.BeginAsyncWork(run_loop.QuitClosure());
  run_loop.Run();
  EXPECT_TRUE(foo_.work_done());
}

This is preferred to RunUntilIdle(), which can be flaky if the asynchronous workload involves a task outside of the TaskEnvironment's purview, e.g. a system event, so use RunUntilIdle() with care.

Want to learn more? Read our documentation on threading and tasks or get involved in the migration to TaskEnvironment!