ルールを回避しようとするなら、その根拠を理解すること
原題: Understanding the rationale behind a rule when trying to circumvent it
日本語訳
# タイトル
ルールを回避しようとする際に、その背後にある根拠を理解すること
# 本文
プロセスおよびスレッド関連のコールバック関数を実装するためのベストプラクティスのドキュメントには、以下のように記載されています。
- ルーチンは短く、単純に保つこと。
- プロセス、スレッド、またはイメージを検証するために、ユーザーモードサービスへの呼び出しを行わないこと。
- レジストリへの呼び出しを行わないこと。
- ブロッキングおよび/またはプロセス間通信(IPC)の関数呼び出しを行わないこと。
- 再入可能デッドロック(reentrancy deadlocks)につながる可能性があるため、他のスレッドと同期させないこと。
ここまでは問題ありません。これらのコールバック関数は、迅速に動作する必要があり、ブロッキングしてはならないようです。これらは、プロセスやスレッドの開始・終了時、DLLやEXEのロード・アンロード時、およびその他のさまざまな低レベルイベントが発生したときに呼び出されるコールバックです。
上記のさまざまな禁止事項は、これらのコールバックがプロセスの作成・終了シーケンス中に呼び出されることを示唆しています。したがって、処理に時間がかかると、システム全体の速度を低下させることになります。また、「レジストリへの呼び出しを行わない」といった極端な要件は、システムが内部ロックを保持している間に呼び出される可能性があることさえ示唆しています。
ベストプラクティスのリストは続きます。
- 特に以下のような作業をキューに入れるために、システムワーカー(System Worker)スレッドを使用すること:
- 低速なAPI、または他のプロセスを呼び出すAPI。
- コアサービスのプロセスを中断させる可能性のある、あらゆるブロッキング動作。
つまり、これはコストのかかる作業を、コールバックの外で実行されるコードにオフロード(肩代わり)する方法についての提案です。これにより、コールバック自体はブロッキングを最小限に抑え、高速である必要があることが改めて強調されます。
エンタープライズサポートの同僚は、システムハングアップの原因が、これらのコールバックは迅速に返さなければならないというルールに違反したドライバーであるというケースによく遭遇します。例えば、よくあるアンチパターンとして、コールバックが上記のガイダンスに従ってシステムワーカー・スレッドに作業をキューイングすることから始めるものの、その後、そのワークアイテムが完了するまでブロックしてしまうドライバーがあります。
これは、ルールが存在する理由を理解せずに、ルールに従っているだけのケースです。
ルールは、コールバックが高速であり、迅速に返さなければならないということです。そのドライバーは、作業をシステムワーク・スレッドに委譲することで、法の文言(字面)通りには従っていました。「ワークアイテムを待機してはならない」というルールは存在しないため、これによって同期的な長時間実行される作業を実行するための抜け穴ができると考えたのでしょう。
しかし、「ブロッキングおよび/またはプロセス間通信(IPC)の関数呼び出しを行わない」および「再入可能デッドロックにつながる可能性があるため、他のスレッドと同期させない」というルールは、コールバック内で長時間ブロッキングすべきではないことを明確に示しています。「~しないこと」という禁止事項は、単にコールバックがブロッキングしうる一般的な方法を列挙しているに過ぎないのです。
そして、2020年には、この特定のケースを指摘するためにドキュメントが更新されたようです。
- システムワーカー・スレッドを使用する場合は、作業の完了を待機しないこと。そうすることは、作業を非同期で完了させるためにキューに入れるという目的を台無しにします。
このルールは、すでに「他のスレッドと同期させない」というルールでカバーされていると主張することもできますが、ドライバーベンダーは「でも、私は他のスレッドと同期させていません。イベントに対して同期しているのです!」と解釈したのでしょう。しかし、当然ながら、そのイベントは別のスレッドによってセットされるものなので、実質的には他のスレッドと同期していることになります。
エンタープライズサポートの同僚は、これを「私じゃない、弟だよ」という言い訳だと表現しています。親から「テレビをつけてはいけません」と言われたとき、弟に「テレビをつけて」と頼むようなものです。厳密には、あなたがテレビをつけたわけではありませんが、弟はあなたの指示に従って動いているため、実質的にはあなたがつけたことになります。(契約書に「開示してはならず、また開示させることもできない」といった文言がよく含まれているのは、このためです。そうでないと、「いいえ、私は全く開示していません。ボブに情報を渡しただけで、開示したのはボブです!」と言えてしまうからです。)
ドキュメントは、次のような内容で始まるべきです。
コールバック関数は、ブロッキングすることなく、迅速に作業を完了させなければなりません。複雑な作業を行う必要がある場合や、他のスレッドまたはプロセスと同期する必要がある場合は、システムワーカー・スレッドを使用するなどして、非同期に作業を行ってください。
その上で、ブロッキングとみなされる例のリストを提示すればよいのです。
コールバック関数では、以下のようなブロッキングは許可されません:
その後に、追加の制約を続けることができます。
さらに、コールバック関数では、以下の操作を一切行ってはなりません:
原文(英語)を表示
In the documentation for best practices for implementing process and thread-related callback functions, it calls out
- Keep routines short and simple.
- Don’t make calls into a user mode service to validate the process, thread, or image.
- Don’t make registry calls.
- Don’t make blocking and/or Interprocess Communication (IPC) function calls.
- Don’t synchronize with other threads because it can lead to reentrancy deadlocks.
So far so good. It seems that these callback functions need to operate quickly and cannot block. These are callbacks that are invoked when a process starts or exits, when a thread starts or exits, when a DLL or EXE is loaded or unloaded, and various other low-level events.
The various prohibitions above suggest that these callouts are called during the process creation/termination sequence, so if you take a long time to deal with them, you are slowing down the entire system. And the rather extreme requirements, like “Don’t make registry calls,” suggest that they might even be called while the system holds internal locks.
The list of best practices continues:
- Use System Worker Threads to queue work especially work involving:
- Slow APIs or APIs that call into other process.
- Any blocking behavior that could interrupt threads in core services.
Okay, so this is providing a suggestion on how you can offload expensive work to code running outside the callback. This once again highlights that the callback itself needs to be fast with minimal blocking.
My colleagues in enterprise support often run into cases where the reason for a system hang is a driver violating the rule that these callbacks must return quickly. For example, a common anti-pattern is a driver whose callback starts by following the guidance above to queue work to a System Worker Thread, but then they block until the work item completes.
This is a case of following the rules without understanding why the rules are there.
The rule is that the callback needs to be fast and return quickly. The driver followed the letter of the law by delegating the work to a System Worker Thread, and there’s no rule that says “Don’t wait for work items”, so they must have figured that this gave them a loophole for executing synchronous long-running work.
But the rules “Don’t make blocking and/or Interprocess Communication (IPC) function calls” and “Don’t synchronize with other threads because it can lead to reentrancy deadlocks” make it clear that you shouldn’t be blocking in your callback for extended periods of time. The “Don’t”s are just calling out some common ways that your callback can block.
And it looks like the documentation was updated in 2020 to call out this specific case:
- If you use System Worker Threads, don’t wait on the work to complete. Doing so defeats the purpose of queuing the work to be completed asynchronously.
One could argue that this rule is already covered by the “Don’t synchronize with other threads” rule, but I guess the driver vendor interpreted it as “But I’m not synchronizing with another thread. I’m synchronizing on an event!” But of course, the event is set by another thread, so you are effectively synchronizing with another thread.
My colleague in enterprise support describes this as the “It wasn’t me, it was my brother” excuse. You are told by your parents not to turn on the television set, so you tell your brother to do it. Technically, you didn’t turn the television set on, but in effect, you did because your brother is acting under your instructions. (This is why contracts often contain wording like “may not disclose or cause to be disclosed,” so that you can’t say “No, I totally didn’t disclose it. I gave the information to Bob, and it was Bob who disclosed it!”)
The documentation should open with something like this:
The callback function must perform its work quickly without blocking. If you need to do complex work or synchronize with other threads or processes, do the work asynchronously, such as by using System Worker Threads.
And then it can give a list of examples of things that count as blocking.
Some examples of blocking that is not allowed from the callback function:
And then it can follow up with additional constraints.
Furthermore, the callback function may not perform any of the following operations: