トップ回答者
C++ネイティブアプリで、あるスレッドを別スレッドから正しく中断させたい

質問
-
こんなことできないのかなぁ、という質問です。
言語はC++で、WIN32APIを直接たたくネイティブアプリです。
スレッドを起動したあとでそのスレッドが実行している処理を外部から中断させる場合、
C++ではデストラクタによる後始末(クリティカルセクションのロック解放とか含む)をするため、
Suspend系やTerminate系のAPIはコールせず、フラグなどを時々監視するコードを
スレッドで実行させる関数の所々に埋め込んでいくのが定石かと思っています。
しかし、この方法だと以下の問題があります。
・監視コードを埋め込むのに多大な労力が発生するし、抜けが生じる。
プロジェクトメンバ全員が、最初の実装段階で適切に監視コードを埋め込めるなどという仮定は当然できないので、
テストに多大な工数がかかってしまいまいす。
・すぐ中断させられない。
たとえば中断フラグ確認直後にコンテキストスイッチして、中断理由となる事象を検出することがある。
そこで、何らかの手段によって外部から直接中断させる、
たとえばそのスレッドのある時点で処理にC++例外が発生したような挙動を発生させる手段が
ないでしょうか?
またあるとして、それは移植性のある手段なのでしょうか?
回答
-
以前に似たようなことを実装したことがあります。コードは手元にありませんが,以下のような感じのコードを書いたと思います。
【事象検出スレッドの処理(x86)】
(1) SuspendThread()で対象となるスレッドの動作を一時中断
(2) GetThreadContext()で,対象となるスレッドのスレッドコンテキスト(CONTEXT)を取得
(3) CONTEXTのプログラムカウンタ(Eip)の値をスタックポインタ(Esp)の示す領域に入れて,
Espをずらす。(関数の戻りアドレスの設定)
(4) CONTEXTのEipを,実行したい関数のアドレスに変更
(5) SetThreadContext()を呼び出して,対象スレッドのスレッドコンテキストを更新
(6) ResumeThread()で停止させたスレッドの処理を再開。かなり強引な方法ですが,一応,これで(4)で設定した関数が,割り込み関数として実行されていました。
ただ,正当な方法ではありませんので,動作確認は充分に行ってください。- 回答としてマーク 山本春海 2011年5月11日 6:39
すべての返信
-
わかりにくくて申し訳ありません。 想定としては次のようなものです:
中断したいスレッドは、「何らかのイベントを待って処理を行う」のが目的ではありません。
「本来すべき処理が何らかの事象で中断せざるを得なくなる」ということに対処したいのです。
また、中断せざるを得ない事象が発生するタイミングは、スレッドが本来すべき処理の論理的な区切りとは
まったく関係なく発生する、という感じです。そうすると、中断したいスレッドのほうで
「自分が本来すべき処理とは関係ない事象監視コードを所々に埋め込む」
という対応が必要になるはずです。この事象監視コードが、イベントオブジェクトを待機時間0msecでWaitForSingleObject()しようと、
bool変数を確認しようと、中断スレッドから見たら扱いは同じかと思います。この「中断したいスレッド」というのが、私が今対応しているシステムにおいてはクラス数2000くらいあるので、
中断の品質(=事象監視コードの埋め込み箇所の品質)を保つのが大変なのです。そこで、どんなことができたらいいかというと・・・たとえば。
「あるAPIをコールすると、指定されたスレッドのコンテキストにおいて、
現在の実行位置(コールスタック/プログラムカウンタ)
に関係なく、ある関数をコールバックするよう割り込ませる」という機能があれば、そのコールバック関数でC++例外をthrowするように実装しておくことにより
そのスレッドの処理を途中で停止させることができます。この例での実装イメージとしては、以下の2つの仮想APIがあるとして
・void _set_thread_injector(void (*pFunc)()); →pFuncを呼び出しスレッドのコールバック関数として登録する。 ・void _call_injected(HANDLE hThread); →hThreadにて識別されるスレッドについて、今の実行を一時停止し、 あらかじめ登録されたコールバック関数を割り込み実行させる。
事象検出スレッドのスレッド関数:
DWORD observe(void* pParam) { while(true) { if(needAbort()) { ::_call_injected(g_hProcessor); } } return 0; }
中断させたいスレッドのスレッド関数:
DWORD process(void* pParam) { ::_set_thread_injector(&throwAbortException); Object* pObj = static_cast<Object*>(pParam); pObj->process(); }
という感じになります。
また Object::process() の中では事象監視コードを一切書かなくてもよい、という感じです。
-
まだ言葉足らずであるように思うので、上記サンプルについて補足します。
_call_indected()が呼び出されると、throwAbortException()関数が
「process(void*)を実行しているスレッドのコンテキスト」で、
「pObj->process()の処理を一時停止」して
呼び出される、ということがキモです。
throwAbortException()にてC++例外をthrowするように実装すると、
_call_injected()によって停止されたインストラクションにおいて
C++例外が発生したように振る舞う、というのが期待する動作です。
これによってpObj->process()内でAuto変数として確保されているインスタンスの
デストラクタが起動するため、そのスレッドを適切に後始末できる、という想定です。
※この動作は、
・VisualStudioのコンパイラオプションで/EHaをつけて
・内部でC++例外をthrowする構造化例外コールバックを実装し
・_set_se_transrator()によってその構造化例外コールバックを登録して
・アクセス違反や0除算を発生させる
ということをやったときに起こる動作に似せて想定しているものです。
-
以前に似たようなことを実装したことがあります。コードは手元にありませんが,以下のような感じのコードを書いたと思います。
【事象検出スレッドの処理(x86)】
(1) SuspendThread()で対象となるスレッドの動作を一時中断
(2) GetThreadContext()で,対象となるスレッドのスレッドコンテキスト(CONTEXT)を取得
(3) CONTEXTのプログラムカウンタ(Eip)の値をスタックポインタ(Esp)の示す領域に入れて,
Espをずらす。(関数の戻りアドレスの設定)
(4) CONTEXTのEipを,実行したい関数のアドレスに変更
(5) SetThreadContext()を呼び出して,対象スレッドのスレッドコンテキストを更新
(6) ResumeThread()で停止させたスレッドの処理を再開。かなり強引な方法ですが,一応,これで(4)で設定した関数が,割り込み関数として実行されていました。
ただ,正当な方法ではありませんので,動作確認は充分に行ってください。- 回答としてマーク 山本春海 2011年5月11日 6:39
-
tanak-muさん、お返事ありがとうございます。
この方法はtotojoさんにご案内いただいたURLに記載の内容と同じですね。
残念ながら上記の通り、
・カーネルモードAPI
・デストラクタ
について実行中にEIP書き換えを行うと正しく動作しなくなってしまうので、
私のプロジェクトでは実用できないと思っております。
今、関数の前後でabort確認を行うマクロを用いて、簡易的に中断させる仕組みを
導入しようと思っており、設計中です。
使い勝手としてはこんな感じ:
void HogeThread::doSomething() { BEGIN_FUNCTION(); // ここで実処理。 END_FUNCTION(); // ここでスレッドの中断フラグを確認。 } void Aborter::abort() { m_pTargetThread->abort(); // これを呼ぶと中断フラグを立てる。 }
これまで作った全関数にマクロを組み込むので、全機能テストしなおしなのが難点ですが。
作り終わったらどこかで公開します。
※このフォーラム、ソースコードの添付機能はないんでしょうか?
-
話的には既に収束しているみたいですが。
せっかくC++を使っているんだし、自前でスレッドの基本クラスを作ってその中に中断の仕組みを組み込んでしまい、
各スレッドの処理の部分を純粋仮想関数で定義しておいて派生側で埋めるようにするとチェック処理の抜けがなくなるんじゃないかと
思いました。基本クラスを作ってそこにフレームワークになる部分を実装しておき、各々で処理する部分だけを派生側の仮想関数で
実装すれば、処理を書く側は特に気にする必要がなくなると思うのです。あとは、処理途中で抜ける事が出来るように中断チェックのメソッドも用意しておいてその場合のみ実装者にコードを書かせる
と最悪でも一つの処理が終わるタイミングでは中断チェックができると思います。
後はフレームワークの実装次第かなと思います。
解決した時は、参考になったレスポンスの所にある[回答としてマーク]ボタンをクリックしてスレッドを締めましょう。 -
PATIO様
お返事ありがとうございます。
説明不足で申し訳ありません。
4/18 7:24の私のレスは、PATIO様のおっしゃるようなことと方向性としてはほぼ同じことをやろうと意図したものです。
私が元々解決したかったことは、そのようなスレッドクラスを作ったとして、実際に中断フラグのチェックを
具象スレッド側で埋め込むのが大変なので、その手間を軽減できないか、というものです。
私のプロジェクトでは、
- 各具象スレッドクラスは、単一の基本スレッドクラスから派生しています。
- 元々(スレッドクラスとは別に)関数の開始と終了に決まったマクロを埋め込むことになっております。
既存のスレッドクラスもそのルールにしたがってすべてマクロを埋め込み済みなので、
そのマクロを書き換えて中断フラグをチェックするようにすることで対応しようと考えています。
(少なくとも、関数の開始タイミングでは自動的に中断がチェックされるようになります)
void ConcreteThread::doSomething() { BEGIN_FUNCTION(); // すでに実装されているこのマクロにて中断フラグをチェック。 // 実処理。 END_FUNCTION(); }
あとは個々のメンバーが非常に長い処理を1関数で書いている箇所や、マクロ埋め込み忘れを機械的に探す感じですね。
すでに2000も定義されている具象スレッドクラスにおいては、親クラスに処理を渡すタイミングが
ほとんどない設計になっています。
したがっていくら親クラス側に中断フラグチェックのルーチンを埋め込んでも、残念ながら効果はない状況です。
-
すでに2000も定義されている具象スレッドクラスにおいては、親クラスに処理を渡すタイミングが
ほとんどない設計になっています。
したがっていくら親クラス側に中断フラグチェックのルーチンを埋め込んでも、残念ながら効果はない状況です。
残念ながら 銀の弾丸 はありません。
設計がまずかったとして諦めて、工数を採っていくか、中断を仕様面で削るしかないと思われます。どこか一カ所に書けばよいなんてことはありません。
具象クラスがどのタイミングで中断をチェックしなければならないか、どういった後処理をしなければならないかを基底クラスが知ることはできません。
これは具象クラスの役割と言えるでしょうから、入れていくしかないと思いますよ。# 2,000 って結構な数だなぁ。
質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。 -
具象スレッドクラスが2000ってのは凄いですねぇ…。きっとLinuxカーネルとかよりも大きなシステムを作られてるんでしょうね。そこまでやるならOSから設計された方がいいのでは?
ふとEIPが~という話も出てきていたので、これはRIPの話になってしまいますが、User-Mode SchedulingというものがWindows Server 2008 R2 / Windows 7 (64bit)から導入されています。私自身この機構でどこまでできるかわかりませんが、スケジューリングできるということは外から止めたり都合のいい処理ができるかもしれませんね。
-
Azulean様、佐祐理様
お返事ありがとうございます。
雑談になりつつありますが。
スレッド数が2000もあるのは機構設計がとてもヘボいためです。
しかし今更後戻りもできず・・・という感じで。
そんなヘボい技術力ですので、C++やOSに手を入れるなどとてもとても・・・。
# ちなみにシステムはFA系で複数PC&多数プロセスで構成されており、スレッドのほとんどは装置制御シーケンスです。
# 各所の動作パターンそれぞれが別スレッドクラスになっているという・・・。
Azulean様のおっしゃることがC++の正道であることは重々承知なのですが、
- 本来の処理とは別の処理を間に挟んで読みにくくしたくない(エラーコードreturnより例外throwのほうが・・・みたいな話に通じるような感じで)
- 工数/予算が足りない
あたりの理由から、本スレッドでのアイディアを考えました。
User-Mode Schedulingの記事の紹介、ありがとうございます。
今回はXP Embeddedなので利用できませんが、なかなか面白そうです。