質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
COM

COM(Component Object Model)はMicrosoftによるコンポーネントテクノロジーであり、 ソフトウェアの再利用を目的とした技術を指します。

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

5回答

6455閲覧

何故デッドロックしない

MagoCat

総合スコア86

COM

COM(Component Object Model)はMicrosoftによるコンポーネントテクノロジーであり、 ソフトウェアの再利用を目的とした技術を指します。

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

2グッド

4クリップ

投稿2016/09/18 21:33

編集2016/09/20 18:42

###概要
COMのスレッドモデルについて理解を深めるためにサンプルを作製して実験していたのですが、実験結果を上手く考察出来ません。もやもやして気持ち悪いのでどなたか考察のお手伝いをしてくださいませんでしょうか。

###実験
VisualStudio2015のATLプロジェクトを使用して、MyATLTestClassというインプロセスサーバを作製しました。クラスのスレッドモデルには"アパートメント"(STA)を選択しました。
MyATLTestClassには、GetThreadIDというメソッドを追加しました。

C++

1// 現在のスレッドのIDを返す。 2STDMETHODIMP CMyATLTestClass::GetThreadID(LONG* id) 3{ 4 // Processthreadsapi.h のGetCurrentThreadIDを使用。 5 *id = GetCurrentThreadId(); 6 return S_OK; 7}

また、このCOMの動作を確認するため、WindowsFormsアプリケーション(C#)を作製しました。
参照から先程のCOMクラスを追加して、以下のようにコーディングしました。

C#

1 public partial class Form1 : Form 2 { 3 //ATLプロジェクトで作ったCOMクラス 4 ATLTestLib.MyATLTestClass myAtl = new ATLTestLib.MyATLTestClass(); 5 6 public Form1() 7 { 8 InitializeComponent(); 9 } 10 11 private void button1_Click(object sender, EventArgs e) 12 { 13 //ボタン1では、GetThreadIDをメインスレッドから呼ぶ。 14 int threadID = 0; 15 myAtl.GetThreadID(out threadID); 16 17 //メッセージボックスを使って表示 18 MessageBox.Show("ボタン1 ThreadID=" + threadID); 19 } 20 21 private void button2_Click(object sender, EventArgs e) 22 { 23 //ボタン2では、GetThreadIDをTask内(別スレッド)で呼ぶ。 24 int threadID = 0; 25 Task task = Task.Run(() => { myAtl.GetThreadID(out threadID); }); 26 27 //タスクの完了を待機してから、メッセージボックスを使って表示 28 task.Wait(); 29 MessageBox.Show("ボタン2 ThreadID=" + threadID); 30 } 31 }

実行結果は以下の画像のとおりです。
イメージ説明 イメージ説明

###考察

メインスレッド上から呼び出そうが、Task内から呼び出そうが、COMオブジェクトのGetThreadIDは同じ値を返しました。これは、COMのスレッドモデルをSTAとしたことから来る挙動だと私は考えています。スレッドモデルをSTAとした場合、COMオブジェクトはそのオブジェクトが作成されたSTAスレッド(今回の実験で言うとWindowsFormsアプリのメインスレッド)に属します。STAスレッドに属するCOMオブジェクトのメソッドが別のスレッドから呼び出された場合、STAスレッドのメッセージループを利用してメソッド呼び出しが同期されるような機構が働くため、Task内での呼び出しにも関わらずGetThreadIDの実行はメインスレッドで行われたものと考えられます。

###考察の穴

上記の"考察"に穴を発見しました。

Task内でのGetThreadID()呼び出しがメッセージに変換されメインスレッドで処理されたと考えるのであれば、不思議な点があります。
Button2クリック時の処理がすべて完了しないことには次のメッセージは処理されないはず、すなわちGetThreadIDの処理命令が実行されないはずです。一方でButton2クリック時の処理はGetThreadIDの完了を待ち受けてから終了します。これはいわゆるデッドロックの形ではないでしょうか。
考察をすすめるうちに、そもそもButton2を押したときに固まらなかったことが不可解に思えてきました。なぜデッドロックしないのでしょうか?

###助けて欲しいこと

考察の穴を解消してください。


【追記】
「もしかするとCOMオブジェクト生成時に別スレッドが起動されその中でメッセージループが回っているからデッドロックが回避されているのではないか」との指摘がありましたので、メインスレッドのスレッドIDを(COMを経由せず)表示するコードをForm1に追加してもう一度実験を行いました。
追加したコードは下記のとおりです。

C#

1 private void button0_Click(object sender, EventArgs e) 2 { 3 //ボタン0では、 4 //AppDomain.GetCurrentThreadIDをメインスレッドから呼ぶ。 5 int threadID = AppDomain.GetCurrentThreadId(); 6 7 //メッセージボックスを使って表示 8 MessageBox.Show("ボタン0 ThreadID=" + threadID); 9 }

結果は以下のようになりました。
イメージ説明 イメージ説明 イメージ説明

ボタン0を押したときに使用しているAppDomain.GetCurrentThreadIDはObsoleteのようです。
よって、ボタン0を処理しているときのスレッドIDを念のためデバッガ上の"スレッド"の表示においても確認しています。

イメージ説明

結果として、以下3つが全て同一のスレッドIDを示すことが確認できました。
・メインスレッドのスレッドID
・メインスレッドから呼び出されたCOMのGetThreadIDが返すスレッドID
・別スレッドから呼び出されたCOMのGetThreadIDが返すスレッドID

つまりは、Taskを使用して別スレッドから呼び出されたCOMのGetThreadIDは確実にメインスレッドのメッセージループで実行されているようなので、Button2内のWait()でデッドロックしない理由については未だ納得できない状態です。


【追記2】
「もしかするとTask.Waitでメインスレッドが完全にブロッキングされる訳ではなくCOMメッセージの処理に限っては継続されるのではないか」という旨の指摘をいただきましたので、Trace.WriteLineを用いて処理順序を追ってみました。

具体的には、ボタン2クリック時の処理にTrace.WriteLineを何行か追加して、次のようにしました。

C#

1private void button2_Click(object sender, EventArgs e) 2{ 3 //ボタン2では、GetThreadIDをTask内(スレッドプール)で呼ぶ。 4 int threadID = 0; 5 Task task = Task.Run(() => 6 { 7 Trace.WriteLine("TaskStart"); 8 myAtl.GetThreadID(out threadID); 9 Trace.WriteLine("TaskEnd"); 10 }); 11 12 //タスクの完了を待機してから、メッセージボックスを使って表示 13 Trace.WriteLine("BeforeWait"); 14 task.Wait(); 15 Trace.WriteLine("AfterWait"); 16 MessageBox.Show("ボタン2 ThreadID=" + threadID); 17}

デバッグ実行してボタン2をクリックしたときの出力画面の様子は以下のとおりです。

イメージ説明

TaskStart->BeforeWait->TaskEnd->AfterWait の順で処理が進行したようです。

"BeforeWait"が出力された直後にはtask.Wait();が行われてメインスレッドがブロッキングされている。
にも関わらず、メインスレッドのメッセージループで実行されるべきmyAtl.GetThreadID(out threadID);を含むタスクが無事終了していることが "TaskEnd" の出力から分かります。

ここで、myAtl.GetThreadID(out threadID);の行がtask.Wait();よりも前に処理されたかtask.Wait();の中で処理されたかですが、中で処理されたと考えるほうがありえそうに思います。なぜならtask.Wait();よりも前には他のメッセージの割り込みを許しそうなコードは一切ないからです。

かといってtask.Wait();によるメインスレッドのブロッキング中に、COMのメッセージだけは選択的に処理されるという動作が自然かというと私は結構不自然だと思いました。

この挙動を説明するMSDNの記述を見つけたいところですね。


【追記3】

[追記2]の実験に幾つかの改良を加えました。

  • メインスレッドのtask.Wait();到達よりもタスク内でのmyAtl.GetThreadID();実行が確実に遅れるように、タスク内の最初で数秒スリープするようにした。

  • デバッグ出力文字列にスレッドIDを添えるようにした。

  • COMメソッド内部でもデバッグ文字列を出力するようにした。

具体的には、まずCMyATLTestClassのGetThreadID()の中身を以下のように変更しました。

c++

1STDMETHODIMP CMyATLTestClass::GetThreadID(LONG* id) 2{ 3 *id = GetCurrentThreadId(); 4 5 CString str; 6 str.Format(_T("InGetThreadID ThreadID=%d\n"), *id); 7 OutputDebugString(str); 8 9 return S_OK; 10}

また、このCOMを利用するWindowsForms側のButton2クリック時の処理を以下のように変更しました。

c#

1private void button2_Click(object sender, EventArgs e) 2{ 3 //ボタン2では、GetThreadIDをTask内(スレッドプール)で呼ぶ。 4 5 int threadID = 0; 6 Task task = Task.Run(() => 7 { 8 //3秒待機すれば、 9 //メインスレッドは間違いなくWait()に突入しているだろう。 10 Thread.Sleep(3000); 11 12 Trace.WriteLine("TaskStart ThreadID=" + AppDomain.GetCurrentThreadId()); 13 myAtl.GetThreadID(out threadID); 14 Trace.WriteLine("TaskEnd ThreadID=" + AppDomain.GetCurrentThreadId()); 15 }); 16 17 //タスクの完了を待機してから、メッセージボックスを使って表示 18 Trace.WriteLine("BeforeWait ThreadID=" + AppDomain.GetCurrentThreadId()); 19 task.Wait(); 20 Trace.WriteLine("AfterWait ThreadID=" + AppDomain.GetCurrentThreadId()); 21 MessageBox.Show("ボタン2 ThreadID=" + threadID); 22}

Button2をクリックすることに依るデバッグ出力の結果は以下のように成りました。
(折角なので、紹介して頂いたDebugViewを使用しています)

イメージ説明

順序はこうです。

1. "BeforeWait"がメインスレッド(ID=6128)で出力された。

2. "TaskStart"が別スレッド(ID=7964)で出力された。
(タスクの最初に3秒スリープしたことで、1と2の出力には大きな時間差があります。この間にメインスレッドは確実にtask.Wait();に突入しているでしょう。)

3. "InGetThreadID"がメインスレッド(ID=6128)で出力された。
(メインスレッドはtask.Wait();でブロックされているはずなのにメインスレッドでCOMのメソッドが実行されています)

4. "TaskEnd"が別スレッド(ID=7964)で出力された。

5. "AfterWait"がメインスレッド(ID=6128)で出力された。
(このタイミングでようやくメインスレッドのブロッキングは解除されています)

つまりこの実験から何が分かるのかというと、task.Wait();による待ちでスレッドの処理が完全にブロックされるのではなくて、別スレッドからSTA-COMメソッドを呼び出したときに発生するメッセージは特別扱いで処理できるようだということです。

追伸:皆さんからコメントで頂いたURLはまだ読んでいないので、今日明日のうちに目を通してみます。

mituha, ibot0709👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答5

0

ベストアンサー

デバッガでCOMオブジェクト側をデバッグし、STDMETHODIMP CMyATLTestClass::GetThreadID(LONG* id)にブレークポイント張れば、状況がはっきりしますね。

結論としては、task.Wait()内で、独自のメッセージループが回っているので、デッドロックしないという結論になります。

下記のコールスタックの 16~26辺りで、PeekRPCAndDDEMessage()という関数名から、全てのウィンドウメッセージを処理するのではなく、RPC(COMの通信)やDDEのメッセージを選択的に処理している感じですね。

また、CLRの実装詳細は不明なので想像の世界ですが、task.Wait()は常に独自メッセージループを回すのではなく、COM呼び出し中と認識している時のみ、COMの呼び出しでデッドロックが発生しないように独自のメッセージループを回しているという可能性もありますね。

以下は、Win10(64bit).NetFramework 4.5.2 環境でDebugビルドのテストプログラムを実行したときのコールスタックです。


  1. ATLTest.dll!CMyATLTest::GetThreadID(long * id) 行 14
  2. rpcrt4.dll!_Invoke@12()
  3. rpcrt4.dll!NdrStubCall2()
  4. combase.dll!CStdStubBuffer_Invoke(IRpcStubBuffer * This, tagRPCOLEMESSAGE * prpcmsg, IRpcChannelBuffer * pRpcChannelBuffer) 行 1449
  5. oleaut32.dll!CUnivStubWrapper::Invoke()
  6. combase.dll!ObjectMethodExceptionHandlingAction<<lambda_1ba7c1521bf8e7d0ebd8f0b3c0295667> >(InvokeStubWithExceptionPolicyAndTracing::__l6::<lambda_1ba7c1521bf8e7d0ebd8f0b3c0295667> action, ObjectMethodExceptionHandlingInfo * pExceptionHandlingInfo, ExceptionHandlingResult * pExceptionHandlingResult, void *) 行 91
  7. combase.dll!DefaultStubInvoke(bool bIsAsyncBeginMethod, IServerCall * pServerCall, IRpcChannelBuffer * pChannel, IRpcStubBuffer * pStub, unsigned long * pdwFault) 行 1891
  8. combase.dll!ServerCall::ContextInvoke(tagRPCOLEMESSAGE * pMessage, IRpcStubBuffer * pStub, CServerChannel * pChannel, tagIPIDEntry * pIPIDEntry, unsigned long * pdwFault) 行 1541
  9. combase.dll!AppInvoke(ServerCall * pServerCall, CServerChannel * pChannel, IRpcStubBuffer * pStub, void * pv, void * pStubBuffer, tagIPIDEntry * pIPIDEntry, WireLocalThis * pLocalb) 行 1604
  10. combase.dll!ComInvokeWithLockAndIPID(ServerCall * pServerCall, tagIPIDEntry * pIPIDEntry, bool * pbCallerResponsibleForRequestMessageCleanup) 行 2722
  11. combase.dll!ThreadWndProc(HWND__ * window, unsigned int message, unsigned int wparam, long params) 行 734
  12. user32.dll!__InternalCallWinProc@20()
  13. user32.dll!UserCallWinProcCheckWow()
  14. user32.dll!DispatchMessageWorker()
  15. user32.dll!_DispatchMessageW@4()
  16. combase.dll!CCliModalLoop::PeekRPCAndDDEMessage() 行 2796
  17. combase.dll!CCliModalLoop::FindMessage(unsigned long dwStatus) 行 2867
  18. combase.dll!CCliModalLoop::HandleWakeForMsg() 行 2484
  19. combase.dll!CCliModalLoop::BlockFn(void * * ahEvent, unsigned long cEvents, unsigned long * lpdwSignaled) 行 2396
  20. combase.dll!ClassicSTAThreadWaitForHandles(unsigned long dwFlags, unsigned long dwTimeout, unsigned long cHandles, void * * pHandles, unsigned long * pdwIndex) 行 53
  21. combase.dll!CoWaitForMultipleHandles(unsigned long dwFlags, unsigned long dwTimeout, unsigned long cHandles, void * * pHandles, unsigned long * lpdwindex) 行 123
  22. clr.dll!MsgWaitHelper(int,void * *,int,unsigned long,int)
  23. clr.dll!Thread::DoAppropriateWaitWorker(int,void * *,int,unsigned long,enum WaitMode)
  24. clr.dll!Thread::DoAppropriateWait(int,void * *,int,unsigned long,enum WaitMode,struct PendingSync *)
  25. clr.dll!CLREventBase::WaitEx(unsigned long,enum WaitMode,struct PendingSync *)
  26. clr.dll!CLREventBase::Wait(unsigned long,int,struct PendingSync *)
  27. clr.dll!Thread::Block(int,struct PendingSync *)
  28. clr.dll!SyncBlock::Wait(int,int)
  29. clr.dll!ObjectNative::WaitTimeout(bool,int,class Object *)
  30. mscorlib.ni.dll!72eda507()
  31. [下のフレームは間違っているか、または見つかりません。ネイティブ デバッガーはマネージ コール スタックのウォークを試みています]
  32. mscorlib.ni.dll!72ea8bac()
  33. mscorlib.ni.dll!72efc69f()
  34. mscorlib.ni.dll!72ece023()
  35. mscorlib.ni.dll!72f29fa8()
  36. mscorlib.ni.dll!72ecdedb()
  37. mscorlib.ni.dll!736eb9e8()
  38. 019f09c2()
  39. System.Windows.Forms.ni.dll!6bcbf5ed()
  40. System.Windows.Forms.ni.dll!6bcc1b08()
  41. System.Windows.Forms.ni.dll!6c2afb78()
  42. System.Windows.Forms.ni.dll!6c26f0f4()
  43. System.Windows.Forms.ni.dll!6c5dee61()
  44. System.Windows.Forms.ni.dll!6bccd433()
  45. System.Windows.Forms.ni.dll!6bccd2f0()
  46. user32.dll!__InternalCallWinProc@20()
  47. user32.dll!UserCallWinProcCheckWow()
  48. user32.dll!DispatchMessageWorker()
  49. user32.dll!_DispatchMessageW@4()
  50. System.Windows.Forms.ni.dll!6bd281ec()
  51. System.Windows.Forms.ni.dll!6bd281ec()
  52. System.Windows.Forms.ni.dll!6bcdc330()
  53. 019f048b()
  54. clr.dll!_CallDescrWorkerInternal@4()
  55. clr.dll!CallDescrWorkerWithHandler(struct CallDescrData *,int)
  56. clr.dll!MethodDescCallSite::CallTargetWorker(unsigned __int64 const *)
  57. clr.dll!RunMain(class MethodDesc *,short,int *,class PtrArray * *)
  58. clr.dll!Assembly::ExecuteMainMethod(class PtrArray * *)
  59. clr.dll!SystemDomain::ExecuteMainMethod(struct HINSTANCE__ *,unsigned short *)
  60. clr.dll!ExecuteEXE(struct HINSTANCE__ *)
  61. clr.dll!__CorExeMainInternal@0()
  62. clr.dll!__CorExeMain@0()
  63. mscoreei.dll!__CorExeMain@0()
  64. mscoree.dll!_CorExeMain_Exported()
  65. kernel32.dll!@BaseThreadInitThunk@12()
  66. ntdll.dll!__RtlUserThreadStart()
  67. ntdll.dll!__RtlUserThreadStart@8()

投稿2016/09/21 12:21

KenjiToriumi

総合スコア344

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

MagoCat

2016/09/21 22:26

おお、わざわざビルドしてデバッグして頂けるとは! ありがとうございます。 > 結論としては、task.Wait()内で、独自のメッセージループが回っているので、デッドロックしないという結論になります。 > 下記のコールスタックの 16~26辺りで、PeekRPCAndDDEMessage()という関数名から、全てのウィンドウメッセージを処理するのではなく、RPC(COMの通信)やDDEのメッセージを選択的に処理している感じですね。 この結論に至ることが出来て、スッキリしました。 (短い実験コードの動作の意味を理解するのにこんなに思い悩むことになろうとは・・・。COMは奥が深そうですね)
guest

0

コメントにぶら下がり過ぎたのでこちらに。

"BeforeWait"が出力された直後にはtask.Wait();が行われてメインスレッドがブロッキングされている。

にも関わらず、メインスレッドのメッセージループで実行されるべきmyAtl.GetThreadID(out threadID);を含むタスクが無事終了していることが "TaskEnd" の出力から分かります。

現状のままだと、推測なだけなので、Task内の先頭にSleepを入れる等して、確実にWaitより後にGetThreadID処理がくるようにしたほうが良いと思います。

COMについては詳しくないので答えは出せませんが、
https://msdn.microsoft.com/ja-jp/library/54z06b18(VS.80).aspx
に関連しそうなことがありますね。
あと、メッセージの再入とかで処理を受け付ける場合もあったような気もするので、
ブロック前に処理されていれば動作するかもしれません。

投稿2016/09/20 06:30

mituha

総合スコア385

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Chironian

2016/09/20 08:12

> Waitでブロックされる前にGetThreadIDが呼び出されている(つまりブロックされない)可能性があるんじゃないかということだけです。 私はメイン・スレッド実行中(BeforeWaitからtask.Wait()直前までの間)に、その同じ【メイン・スレッド】で別の処理(GetThreadID)が実行される可能性はないと理解しています。つまり、BeforeWaitからtask.Wait()直前までの間もGetThreadID要求が待たされることは確実なので確認不要と考えています。 確実性に不安を感じる方は目を瞑るのではなく、積極的に確認した方が良いと思います。
mituha

2016/09/20 09:58

通常のスレッドであればその通り(同じメインスレッドでの動作はない)だと思います。 で、メッセージ処理が絡む場合はそのような動作ではないのではというのがこの質問の趣旨と捉えています。 そもそも、通常のスレッド処理であれば、task.Wait()でデッドロックは確実です。 task.Waitでメッセージループが回される場合で、再入的にメッセージが処理されれれば、Waitはデッドロックにならないと考えられます。 で、Waitより前のタイミングでもメッセージループが回る場合があるとすれば、 Waitより前にTask内のGetThreadIDが動作する可能性もあるのではという話です。 実際にどうか等は質問者さんに確認したいただけたらなと、見守っています。
MagoCat

2016/09/20 13:08

お返事遅れました。 Chironianさんの 「CMyATLTestClass::GetThreadID(LONG* id)の中でOutputDebugString()」 と mituhaさんの 「Taskの先頭でSleep」 を取り入れてもう一度実験してみようと思います。 確かに、"BeforeWait"の出力(とほぼ同時のtask.Wait();)の数秒後に"TaskStart"が出力される様子が観察できれば、Task.Waitによるブロッキング中にGetThreadIdが処理されたことが誰の目からも一目瞭然になりそうですね。 > COMについては詳しくないので答えは出せませんが、 > https://msdn.microsoft.com/ja-jp/library/54z06b18(VS.80).aspx > に関連しそうなことがありますね。 ありがとうございます、拝見してみます。
MagoCat

2016/09/20 16:22

ちょっと今OutputDebugStringの出力が出なくて手間取ってます。 COMのデバッグだと勝手が違うみたいですね。 もうしばらく粘ってみます
Chironian

2016/09/20 16:47

コンパイルには通ってますか? もし通らないなら、OutputDebugStringA("hoge hoge");もしくは、OutputDebugStringW(L"HOGE HOGE");のどちらかで通ると思います。 コンパイルに通っているなら、頭痛そうです。もしかしてデバッグ・モードで起動したexeの出力しかでないとかあるのかも知れません。そうだったらごめんなさい。 私はいつもDebugView.exe(https://technet.microsoft.com/ja-jp/sysinternals/debugview.aspx)を使ってました。 DebugViewでググルと使い方を書いているサイトも多数あります。
MagoCat

2016/09/20 16:58

あ、OutputDebugStringの件、たった今解決しました。 WindowsFormsプロジェクトで [プロパティ]→[デバッグ]→[ネイティブコードのデバッグを有効にする] にチェックする必要があったようです。 お騒がせしました。
Chironian

2016/09/20 17:09

なるほど。 良いことを聞きました。ありがとうございます。
MagoCat

2016/09/20 19:06

新しい実験結果追記しました。 やはりWait()によるブロッキングの最中にCOMのメッセージ処理してる気配が濃厚です。 こういう事が起こるというのは受け入れる他無いので、後はこの現象を説明する文献をいくつか読むことで自分の血肉としたいですね。 幾つか既にコメントでURLを頂いているので、今日明日にでも目を通してみようと思います。 まだ出ていないもので「これは」というのがあれば是非お願いします。
MagoCat

2016/09/21 22:23

> COMについては詳しくないので答えは出せませんが、 > https://msdn.microsoft.com/ja-jp/library/54z06b18(VS.80).aspx > に関連しそうなことがありますね。 Visual J++ 向けの記事のようですが、「STA スレッドがブロックされた場合、通常は共通言語ランタイムが COM オブジェクトに代わって暗黙のポンプを実行します」という共通言語ランタイムの絡んだ文言は今回の問題の核心に近そうですね。 あと、独自にネットで色々見ていて、こんなのも見つけました。 ページの一番下の方に関連しそうな事柄が書いてありました。 https://msdn.microsoft.com/ja-jp/library/74169f59(v=vs.110).aspx
guest

0

こんにちは。

STAスレッドに属するCOMオブジェクトのメソッドが別のスレッドから呼び出された場合、STAスレッドのメッセージループを利用してメソッド呼び出しが同期されるような機構が働くため、Task内での呼び出しにも関わらずGetThreadIDの実行はメインスレッドで行われたものと考えられます。

私もそう思います。

Button2クリック時の処理がすべて完了しないことには次のメッセージは処理されないはず

しかし、こちらは違います。
モーダル・ダイアログを表示していても、当たり前ですがメッセージ・ループは回っています。でないとモーダル・ダイアログを操作できませんので。
従って、COMのメッセージも処理されている筈です。


違いました。
モーダル・ダイアログ表示前のtask.Wait();でメッセージ・ループを回っているスレッドがブロックされているのになぜGetThreadID()が処理されるのか?ですね。
確かによく分からんですね。


【追記】
STAではCOMオブジェクト生成側のメッセージ・ループ(今回の場合メイン・スレッド)を使って同期しているように私も理解してますが、それが誤りかも知れません。

COMオブジェクト生成時に別スレッドを起動し、その中で勝手にメッセージ・ループを回り、それを使って同期しているならデッドロックしないです。
メイン・スレッドのIDを表示してみたらどうでしょうか?(もちろん、COMを経由しないで直接)
メイン・スレッドはtask.wait()でブロックされているので、これを使ってCOMメッセージを処理しているとしたらデッドロックする筈なので、COMへの要求は別のスレッドで実行されている筈です。


【質問の追記を見て追記】
ビックリですね。でも、絶対に説明は付く筈です。
task.Wait()はスレッド終了だけを待っていると思ってましたが、そうではないということかも知れません。task.Wait()がCOMメッセージを処理しつつスレッド終了を待つのであれば説明が付きますね。

 確かC#のTrace.WriteLine()を使えばデバッグ出力(Windows APIのOutputDebugString)ができたと思います。
これを、Task.Run()で起動するサブスレッドの中(*1)と、Task.Run()の後task.Wait()の前(*2)においてみると確認できると思います。
task.Wait()内でCOMメッセージ処理されているなら(*2)→(*1)の順で表示される筈です。この場合、(*2)の後でサブスレッド内のCOM処理がメイン・スレッドで実行されているということになるので、task.Wait()がCOMメッセージを処理している以外に考えられないと思います。

 逆の順で表示されたら、う~ん頭痛いです。Task.Run()の頭はメイン・スレッドで実行しているとか、GetCurrentThreadId()などが疑わしくなりますが、流石にそれはないような気がするので。

投稿2016/09/19 01:16

編集2016/09/19 15:03
Chironian

総合スコア23272

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

MagoCat

2016/09/19 08:16

回答ありがとうございます。 >COMオブジェクト生成時に別スレッドを起動し、その中で勝手にメッセージ・ループを回り、それを使って同期しているならデッドロックしないです。 >メイン・スレッドのIDを表示してみたらどうでしょうか?(もちろん、COMを経由しないで直接) >メイン・スレッドはtask.wait()でブロックされているので、これを使ってCOMメッセージを処理しているとしたらデットロックする筈なので、COMへの要求は別のスレッドで実行されている筈です。 これはいい考えだと思います。早速追加実験してみます。
MagoCat

2016/09/19 09:28

追加実験内容を追記しました。 結果、「COMオブジェクトが独自に別スレッドでメッセージループを回しているからデッドロックしない」というストーリーは無さそうに見えました。
MagoCat

2016/09/19 20:37

>確かC#のTrace.WriteLine()を使えばデバッグ出力(Windows APIのOutputDebugString)ができたと思います。 なるほど、確かにTrace.WriteLine()を使うと処理の流れが追いやすくなりそうです。 Trace.WriteLine()を使用した実験結果を追記しました。 >task.Wait()内でCOMメッセージ処理されているなら(*2)→(*1)の順で表示される筈です。この場合、(*2)の後でサブスレッド内のCOM処理がメイン・スレッドで実行されているということになるので、task.Wait()がCOMメッセージを処理している以外に考えられないと思います。 実験してみて、私も「task.Wait()がCOMメッセージを処理している以外に考えられない」と同じ感想です。 また、「メインスレッドをブロッキングしつつもCOMのメッセージは処理する」という挙動についてのMSDNの記述が欲しいところだと感じました (Task.Wait()にはその記述が無いようなので、Task.Wait()の持つ特質では無いのかもしれません)。
mituha

2016/09/19 23:45

現状のTrace結果だけではタイミング的にはまだ穴がありそうに思えます。 TaskStart -> BeforeWait -> GetThreadID -> Wait と動作した可能性。 Task処理の先頭にSleep等を追加して、確実に遅延処理してはどうでしょう? 後、念には念をいれるのであれば、TaskのThreadIDの表示もあったほうが良い気がします。 結果が気になるので、コメントに割り込んでしまいました。スミマセン。
Chironian

2016/09/20 03:21

MagoCatさん。 今回の確認で十分と思いますが、すいません。COM側はCMyATLTestClass::GetThreadID(LONG* id)の中でOutputDebugString()した方がより安心感がでると思います。 最初そのように思っていたのに回答書いている間に失念してました。 Traceのデフォルトの出力先とOutputDebugString()の出力先は同じですし、きちんと排他制御されてますので、メッセージが入り交じることはないです。 もし、DebugView(https://technet.microsoft.com/ja-jp/sysinternals/debugview.aspx)を使えるようでしたら、Visual Studioの非デバッグ・モードで起動(Ctrl+F5など)すればデバッグ出力をDebugViewへ出力できます。 これは速度も早いし、スレッドIDやメッセージが発行された時刻をmSec単位でも出力してくれるので中々便利です。 > また、「メインスレッドをブロッキングしつつもCOMのメッセージは処理する」と > いう挙動についてのMSDNの記述が欲しいところだと感じました 全くその通りですね。でも、悲しいことによくあることかも... mituhaさん。 BeforeWait -> GetThreadID -> Waitの動きで、GetThreadIDがメイン・スレッドのIDを返却するのはないだろうと思います。経験的にはそこはWindowsを信頼しても良いように感じます。 スレッドの実行をプリエンプティブに取り上げて、更に実行中スレッドとして別の処理を割り込ませるようなOSでは事実上マルチ・スレッド・プログラムをデバックできないと思います。1つのスレッドが複数の処理をプリエンプティブに実行するって悪夢です。
mituha

2016/09/20 03:52

Chironianさん。 > BeforeWait -> GetThreadID -> Waitの動きで、GetThreadIDがメイン・スレッドのIDを返却するのはないだろうと思います。経験的にはそこはWindowsを信頼しても良いように感じます。 流石に、返されるスレッドIDは疑ってません。 TaskのThreadIDの表示はメインスレッドで動作はしない(スレッドプールのスレッドと思います)はずですが、念のためです。 > TaskStart -> BeforeWait -> GetThreadID -> Wait は、現状、出力から、 TaskStart -> BeforeWait -> Wait -> GetThreadID -> TaskEnd -> AfterWait を想定しており、Waitでブロックされると考えておられるようですが、 WaitとGetThreadIDは並列でどちらが先に動作しているかは現状は不明と思われます。 BeforeWait出力直後にGetThreadIDが割り込んで動作しうる。 そこで、Taskの先頭でSleepして、Waitより後にTaskStartがくるようにしたらどうかの提案でした。
Chironian

2016/09/20 04:41

> BeforeWait出力直後にGetThreadIDが割り込んで動作しうる。 そのGetThreadID()が返却するスレッドIDは、BeforeWaitを出力しているスレッドのIDです。 つまり、BeforeWait出力後、task.Wait()に到達する前にGetThreadIDが割り込んで、BeforeWaitのスレッドIDを返却している可能性が排除できていないということですよね? その可能性を排除するには、各出力時に最低秒単位の時刻を表示し、BeforeWait出力後、API呼び出しせずに数秒くらい掛かるforループを回せばできると思います。 私自身はそこまで疑う必要性は感じませんが、人によっては不安でしょうから確認するのも手かもしれません。
mituha

2016/09/20 05:37

> そのGetThreadID()が返却するスレッドIDは、BeforeWaitを出力しているスレッドのIDです。 > つまり、BeforeWait出力後、task.Wait()に到達する前にGetThreadIDが割り込んで、BeforeWaitのスレッドIDを返却している可能性が排除できていないということですよね? すみません、あまり、COMのスレッド回りは詳しくないので論点をずらしてしまったかもしれません。 気になったのはWaitが先に動作してブロックしていると書かれていますが、 Waitでブロックされる前にGetThreadIDが呼び出されている(つまりブロックされない)可能性があるんじゃないかということだけです。 ところで、COMのスレッドでメソッド呼び出し時点のものなんでしょうか? COMのクラス作成時のもののような気もするのですが?
guest

0

デッドロックは起きません。GetThreadIDメソッドはブロッキング操作を含みませんから、デッドロックの要因になり得ません。

デッドロックはブロッキング操作、つまりロックの獲得操作や外部リソースへの依存などの該当ソースコード以外の要因で、該当スレッドの進行が無期限に停止しうる場合にのみ発生リスクがあります。

投稿2016/09/19 01:16

yohhoy

総合スコア6189

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

MagoCat

2016/09/19 08:11

通常はそうですが、COMオブジェクトがSTAスレッドに属しているために、COMオブジェクトへの別スレッドからのメソッド呼び出しがメッセージに変換され、STAスレッドのメッセージループで処理されることで暗黙的に同期実行される様子を示すのが今回の実験です。 メインスレッド上からの呼び出しでも、Task上からの呼び出しでも同じスレッドIDが取得できたことが、暗黙的な同期の存在を裏付けています。 そして私はこの暗黙的に行われている同期がいわゆる"this.Invoke()"と類似の機構だと考えていました。("this.Invoke()"も、メッセージループ上で処理するように要求するメソッドですよね) つまりは、今回の実験には下記のコードでボタン3を押したときに発生するデッドロックと同様のデッドロックを期待する余地があるのです。 public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Hoge() { return; } private void button3_Click(object sender, EventArgs e) { //★これはデッドロック Task task = Task.Run(() => { this.Invoke((Action)(() => Hoge())); }); task.Wait(); } }
yohhoy

2016/09/20 08:32 編集

質問の意図を取り違えていたようですね。失礼しました。 私自身もちゃんと理解できていませんが、とりあえず関連しそうなURLだけ貼っておきます: - https://blogs.msdn.microsoft.com/pfxteam/2009/10/15/task-wait-and-inlining/ - http://stackoverflow.com/questions/21211998/stataskscheduler-and-sta-thread-message-pumping - http://stackoverflow.com/questions/21571598/which-blocking-operations-cause-an-sta-thread-to-pump-com-messages
MagoCat

2016/09/20 13:09

URL 3つもありがとうございます。 拝見してみます。
MagoCat

2016/09/21 22:24

まさに関係のある内容でした。 リンク先の海外掲示板は、一部のブロッキング操作がCOMメッセージをポンプすることについては共通認識のあるところからスタートして更に進んだ話を繰り広げていますね。
guest

0

GetThreadID は単にメソッド実行なので、メッセージ処理とは関連なしに動作しているのではないでしょうか。

投稿2016/09/19 00:18

Tipo

総合スコア239

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

MagoCat

2016/09/19 10:04

回答ありがとうございます。 普通はTaskで呼び出したメソッドは別スレッドで実行されますが、今回のCOMオブジェクトのGetThreadIDはTaskで呼び出したにも関わらず別スレッドではなくメインスレッドで実行されています(取得されたスレッドIDの値から判断)。 これは「単なるメソッド実行」というには多少複雑な機構が介在していることを示して居ます。 メッセージ処理と関係が無いと言うのであれば、この多少複雑な機構が他にどのように説明されうるのかが、私には分かりません。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問