別スレッドでリソースを解放することのあれそれ
Boost.SmartPtr:shared_ptr + weak_ptr(Cryolite) の 24 分目あたり.
shared_ptr<void> による遅延解放 vector<shared_ptr<void *> > to_be_disposed; shared_ptr<HeavyToDispose1> px(…); shared_ptr<HeavyToDispose2> py(…); … // ここで削除して処理が止まると困る… to_be_disposed.push_back(px); px.reset(); to_be_disposed.push_back(py); py.reset(); … // 適当なタイミング or 別スレッドで // to_be_disposed.clear() を実行
の部分に関して,
別のスレッドでのリソース解放は,スレッド親和性をもつリソースを破棄には対応できないことに注意.この辺はCLRのファイナライザスレッドも同様の問題を抱えている #boostjp
2 days ago from MiniTwitter
てな感じにつぶやいた件について,
@NyaRuRu さんの http://twitter.com/NyaRuRu/status/6594169320 の発言がいまいち理解できていないので,誰か優しく教えてくれないかにゃー.
7:48 AM Dec 13th Twitで
という質問を頂いた件について遅くなりましたが補足,というほどでもないんですが,軽くいくつかつらつらと.
以下,基本的に Win32 + .NET (CLR) な文脈で.
ファイナライザスレッド
MS CLR のファイナライザは,ファイナライザスレッドという別のスレッドで実行されます *1.なので,あるリソース解放処理が別スレッドでも行えるか否かという観点は,.NET な人には割とお馴染みなはず.
よく訓練された .NET プログラマは,ファイナライザを書きながら「この処理はファイナライザスレッドで実行しても大丈夫かにぇ?」と呟くといいます[要出典].
スレッド親和性を持つリソースについて
Remarks
A thread cannot use DestroyWindow to destroy a window created by a different thread.
The methods IDirect3DDevice9::Reset, IUnknown, and IDirect3DDevice9::TestCooperativeLevel must be called from the same thread that used this method to create a device.
このように,しれっとドキュメントに制限が書いてあるのですな.コンパイル時エラーにはならないのでほんと注意.
スレッドごとに設定可能なコンテキスト
解放処理を書くときに,思わずコンストラクタと同じ実行コンテキストを仮定しがちなのですが,ファイナライザスレッドのコンテキストがどうなっているかはよく分からんということで注意.
Windows で per thread な設定を思いつくままにいくつか列挙.
- 優先度 / IO 優先度 (SetThreadPriority)
- MMCSS における Tasks タイプ (AvSetMmThreadCharacteristics)
- CPU Affinity (SetThreadAffinityMask)
- COM Threading Model (CoInitialize)
- GDI のバッチ処理限界 (GdiSetBatchLimit)
- Locale (SetThreadLocale)
- UI 用言語 (SetThreadUILanguage)
- ユーザの偽装トークン (SetThreadToken)
- 実行ステート (SetThreadExecutionState)
さらに,ファイナライザ中でこの手の設定を弄ると,後続するファイナライザも影響を受けるわけで.
スレッドプールとかでも似たような話はあるんでしょうけど.
ファイナライザのタイムアウト
vector<shared_ptr<void *> > のように,対象を区別せずに一様に解放処理を行う場合,
- 本当に致命的なことは起きないと仮定する
- 万一致命的なことが起きた場合に備えて Watchdog 等も準備
の判断が場合によっては難しいかもですにゃーとか.
たとえば,全てのファイナライザが実行し終わるまでアプリケーションが終了できないようになっていると,ひとつのファイナライザが無限に待機するだけで永遠にアプリケーションが終了できなくなります.同様に,プラグインを低い権限で動かすようなアプリケーションを作っているときに,プラグインが生成するオブジェクトのファイナライザで無限ループするだけでホストアプリケーション全体がやばいことになってしまうようでは困ります.
とまあそんなこんなで,ファイナライザやアプリケーションの終了処理にタイムアウトを設定できるようにしようとか,本当に重要なファイナライザとそうでないファイナライザを分けようとか,色々と夢や風呂敷が広がったりすると,結局いまの CLR と似たものができあがりそうだったり.
余談: 本当に保護すべきリソースは何か?
微妙に関係ない話ですが,以下のような不具合があるとき,どちらの修正を優先するかという視点もあります.
- 軽微なメモリリークはあるが,いつプロセスを強制終了してもファイルは壊れないモジュール
- 単体では完璧に動くが,特定タイミングで外部からプロセスを強制終了すると確実にファイルが破損するモジュール
Windows アプリケーションは,単にその実行ファイル名が怪しいという理由だけでユーザーに不審に思われてタスクマネージャから殺される世界です.当然デストラクタは実行されません.ログオフ時にちょっと処理が遅れただけでも強制終了されます*2.当然デストラクタは実行されません.
例外安全でメモリリークもなくデストラクタで確実にリソースが解放されるプログラムも大切ですが,どのコード行でプロセスが突然終了しても守られるべきデータは無事というプログラムも大事だと思う今日この頃.