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
なども含まれます7。mmap
でのファイルの読み書きは、予期せぬ場所で入出力待ちが発生する可能性があるため、基本的には使われません。
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::mutex
とstd::unique_lock
相当で独自実装のbase::Lock
8とbase::AutoLock
があります。pthreadやC++標準ライブラリのものと比べて、特殊な部分は特にありません。独自実装を維持する理由も特にないので、いずれstd::mutex
やstd::unique_lock
に移行することになるかと思います。
-
ブラウザプロセス、レンダラプロセスの他に、GPUプロセス、Pluginプロセスなど、他にもいくつか種類がいます。 ↩
-
例えばBlinkのWebKit由来のコードとChromiumのコードで、まだ統合できていない部分によるもの。他、GCに関わる部分。BlinkにはC++部分にもGCがありChromiumには無い。 ↩
-
レンダラプロセスはHTMLやJSなどのuntrustedなコードを扱い、システムへのアクセス権を落とした状態で走る。おおむね1タブに1プロセス作られる。 ↩
-
ブラウザプロセスは全体でひとつ。通常のアプリケーションの権限で走り、ファイルシステムやネットワークへのアクセスは一手に引き受ける。 ↩
-
Windowsでは
WaitForMultipleObjectEx
とPeekMessage
で回る伝統的なWindowsアプリのループ。Androidではandroid.os.Handler
とandroid.os.Looper
、Linuxではglib
を使ったもの。 ↩ -
WindowsではIO Completion Port, 他ではepollやkqueueをlibevent経由で使っている。 ↩
-
Linuxでの/procの読み出しは、ブロックしないのでそのまま実行しています。 ↩
-
WindowsではSlim Reader/Writer Locks, POSIXなOSではpthread_mutexで実装。 ↩