Chromium Browser Advent Calendar 2017の23日目の記事です。

まえおき

Advent Calendarを順に読んできたみなさんは、そろそろChromiumにcontributeしたくなってきている頃かと思います。この記事では、Chromium中で使われるスレッドまわりの内部ライブラリと、標準ライブラリとの違いを紹介します。わりと頻繁に変わるので、自分で使うときには要確認です。

ブラウザプロセス、レンダラプロセス

Chromiumはマルチプロセス1なブラウザですが、Blink部分とそれ以外2、もしくはレンダラプロセス3とブラウザプロセス4ではだいぶ様子が異なります。今回はブラウザプロセスの話。大部分の処理をメインスレッドで行うレンダラプロセスと違って、ブラウザプロセスでは複数のスレッドを飛び回って処理を進めています。

ネームドスレッドとスレッドプール

ブラウザプロセスで使用するスレッドは、UIスレッド,IOスレッドなどの名前付きのものと、スレッドプールに属するものに大別できます。UIスレッドはブラウザプロセスのメインスレッドで、ブラウザUIやChromiumのプロファイル、Extension APIの受け口などを担当しています。IOスレッドはソケットの端点やIPCの受け口で、レンダラからのリクエストの受け答えをしています。

UIスレッドとIOスレッド

UIスレッドとIOスレッドの大きな違いは、スレッドのメッセージループの構造の違いです。
UIスレッドのメッセージループはプラットフォームのネイティブなもの5。IOスレッドのメッセージループはファイルやネットワーク、IPCの完了待ちをできるように6作られています。
半年ほど前までは、UI, IOに加えてFILEスレッド、DBスレッド、CACHEスレッドなど、役割別にスレッドがありましたが、今ではUIとIO以外は非推奨で、スレッドプールに統合されています。

ブロック禁止

重い処理や完了待ちが必要な処理をUIスレッドやIOスレッドでするのは禁物です。ブラウザプロセスのネームドスレッドはブラウザインスタンス全体でひとつずつだけなので、誰かが専有してしまうと、他の全ての処理が滞ってしまいます。
完了待ちが発生しうる処理は、同期版のread, write, connect, acceptはもちろん、syscallに非同期版が存在しないopen, statなども含まれます7mmapでのファイルの読み書きは、予期せぬ場所で入出力待ちが発生する可能性があるため、基本的には使われません。

Threading Primitives

base::Thread, base::MessageLoop, base::TaskScheduler

スレッド周りのライブラリで比較的低レイヤにいるのがこの3つ。base::Threadはその名の通りスレッドの抽象化。プロダクションコードで自分で作ることはあまりなく、ユニットテストの中でスレッドが必要になることはたまにあります。
インスタンスを作ってStart()して、base::BindOnceでタスクを作り、PostTask()でタスクを投げます。後述のbase::MessageLoopは自動で作られます。

void DoSomething() {}

base::Thread thread;
thread.Start();
thread.task_runner()->PostTask(
    FROM_HERE,
    base::BindOnce(&DoSomething);
thread.Stop();

base::MessageLoopは、名前はメッセージループですが、実態はタスクキューです。ユーザ側が呼び出したい関数を引数付きで指定すると、base::MessageLoop中で順番に実行されます。こちらも大抵は他のユーティリティ経由で使うだけで、直接使うことはあまりありません。

base::TaskSchedulerは内部にスレッドプールを持ち、base::TaskRunner経由で投げられたタスクを適当なスレッドで実行します。負荷に応じたスレッドの数やタスクの優先順位を調節も担当しています。

TaskRunner, SequencedTaskRunner, SingleThreadTaskRunner

スレッドを飛び回るときに直接使うのがこれらのbase::TaskRunnerたちです。この順番に継承関係があります。C++20に入るかもしれないexecutorに近いものです。
PostTask()PostDelayedTask()経由渡されたタスクは、裏にいるスレッドやスレッドプール上で適当に実行されます。複数スレッドを使うクラスやコンポーネントを設計するときには、外部からTaskRunnerを指定できるようにして、テスト時にタスクの実行順制御をしやすくするのが一般的です。
base::SingleThreadTaskRunnerに投げられたタスクは常に特定のスレッド上で実行されます。Thread Local Storageを使うコードやThread IDに依存した何かをしているときには、SingleThreadTaskRunnerを使って型レベルで制約を明示します。
base::SequencedTaskRunnerはもう少し制約がゆるく、投げられたタスクは順番に実行され、前のタスクが終わるまでは次のタスクが実行されないことだけを保証します。個々のタスクは別々のスレッドで実行されるかもしれません。複数のタスク間で共有されるデータがある場合でも、タスクが同一のSequencedTaskRunnerで走るのならば、ロックなどでの排他制御は不要です。特に事情がなければこれを使うのがよいかと思います。
base::TaskRunnerには上記の制約はなく、投げられたタスクが適当なスレッドで同時に実行されるのを許します。外部のデータに依存しないタスクに適しています。

base::Bind{Once,Repeating}() & base::{Once,Repeating}Callback<>

base::OnceCallback<>はTaskRunnerに投げるタスクを表現するクラステンプレートで、base::BindOnce()はCallbackを生成する関数です。
C++11のstd::bind()std::function<>に相当するもので、Chromiumでの用途に合わせて、標準ライブラリにあるものとはちょっと違うセマンティクスや制約が入っています。

int Foo(int x, int y) { return x * y; }
base::OnceCallback<int(int)> cb = base::BindOnce(&Foo, 7);
CHECK(std::move(cb).Run(6) == 42);

base::RepeatingCallback<int(int)> cb2 = base::BindRepeating(&Foo, 123);
CHECK(cb2.Run(4) == 492);

この例では、関数FooにBindOnceを使い、引数を一つ部分適用したオブジェクトを作っています。Run()は残りの引数を受け付け、保存されていた引数と一緒にFooを呼び出します。

base::RepeatingCallback<>はコピー可能,何度でも呼び出せるタイプで、std::function<>に近いものです。ただしstd::function<>と違って内部状態を参照カウント型のストレージで持っているので、cb2をコピーしても、内部に持っている123はコピーされません。
base::OnceCallback<>はMove-onlyかつ一度だけ呼び出せるタイプで、C++20に入るかもしれないstd::unique_function<>相当です。base::OnceCallback<>std::unique_function<>と違って内部状態は別なストレージに持っているので、move時にはBindされた引数それぞれはmoveされません。

TaskRunnerに投げるタスクは、引数なし,戻り値なしのOnceCallbackかRepeatingCallbackで、それぞれOnceClosure, RepeatingClosureと別名がつけられています。

スレッド間移動にTaskRunnerとBindを使うときには、RepeatingではなくOnceを使うのがおすすめです。Repeatingの方は内部で参照カウントを使っている関係上、内部ストレージに保存されたオブジェクトが削除されるスレッドが非決定的で、わかりにくいバグが入り込む余地がありますし、Onceの方は保存された引数をrvalueで渡すので、型や値渡しな引数についてはコピーが減る場合があります。また、Onceの方は後述のbase::Passed()を使わなくてもmove-onlyな型をBindできます。

base::Owned(), base::Passed(), base::ConstRef(), base::Unretained(), base::RetainedRef()

base::BindOnce()base::BindRepeating()は、部分適用に使う値を内部に保存しますが、値の保持の仕方をいくらかカスタマイズできます。base::ConstRef()std::cref()相当、Bindされるオブジェクトをbase::ConstRef()で包んでおくと、内部ストレージは参照のみを保持し、コピーしません。
base::Passed()はmove-onlyな型をBindRepeating()で扱う時に必要で、内部ストレージから対象の関数に渡すときにstd::move()します。
生ポインタをbase::Owned()に包んで渡すと、Callbackの破棄と一緒に包まれたポインタもdeleteされます。
参照カウントを持ったオブジェクトへのポインタをRetainedRef()に包んで渡すと、Callbackの寿命にあわせてカウントの上げ下げをしてくれます。
base::Unretained()は、歴史的事情の産物です。諸事情から、Bindのthisポインタの位置に生ポインタが渡されると、参照カウントの上げ下げが試みられますが、base::Unretained()はこの挙動からのopt-outを指示します。

Atomic

基本的に標準ライブラリのstd::atomic<>を使います。歴史的経緯から独自実装のものが残っていますが、新たに何かを書くときには考慮する必要はないかと思います。

Lock

std::mutexstd::unique_lock相当で独自実装のbase::Lock8base::AutoLockがあります。pthreadやC++標準ライブラリのものと比べて、特殊な部分は特にありません。独自実装を維持する理由も特にないので、いずれstd::mutexstd::unique_lockに移行することになるかと思います。


  1. ブラウザプロセス、レンダラプロセスの他に、GPUプロセス、Pluginプロセスなど、他にもいくつか種類がいます。 

  2. 例えばBlinkのWebKit由来のコードとChromiumのコードで、まだ統合できていない部分によるもの。他、GCに関わる部分。BlinkにはC++部分にもGCがありChromiumには無い。 

  3. レンダラプロセスはHTMLやJSなどのuntrustedなコードを扱い、システムへのアクセス権を落とした状態で走る。おおむね1タブに1プロセス作られる。 

  4. ブラウザプロセスは全体でひとつ。通常のアプリケーションの権限で走り、ファイルシステムやネットワークへのアクセスは一手に引き受ける。 

  5. WindowsではWaitForMultipleObjectExPeekMessageで回る伝統的なWindowsアプリのループ。Androidではandroid.os.Handlerandroid.os.Looper、Linuxではglibを使ったもの。 

  6. WindowsではIO Completion Port, 他ではepollやkqueueをlibevent経由で使っている。 

  7. Linuxでの/procの読み出しは、ブロックしないのでそのまま実行しています。 

  8. WindowsではSlim Reader/Writer Locks, POSIXなOSではpthread_mutexで実装。