none
MFCとOpenMPの相性 RRS feed

  • 質問

  • お世話になっております。

    VC2008を使用しております。

    OpenMPが生成されるスレッド内でMFCを使用しても問題ないのでしょうか。

    CreateThreadや_beginthread等で作成されるスレッド内でMFCを使用することは補償されていないことを記憶にあるのですが、

    OpenMPでMFCを使用しても問題ないのでしょうか。

    2011年10月24日 10:37

回答

  • 一般的にライブラリがスレッドセーフという場合、ある関数やメソッドなどへのアクセスを複数のスレッドで同時に行っても破たんしないということを指します。

    例えば、コンソールへの画面出力。ちょっと前にあちこちで話題になっていましたけどw

    puts( "thread 1-call1" );
    puts( "thread 1-call2" );

    という呼び出しを行うスレッドと

    puts( "thread 2-call1" );
    puts( "thread 2-call2" );

    という呼び出しを行うスレッドがあったとした場合。

    これらが本当に同時にアクセスが入ったとしても、

    tthrehradead 21-ccaall12

    などとわけのわからない状況になったりせず、

    thread 1-call1
    thread 2-call1
    thread 1-call2
    thread 2-call2

    と、少なくとも呼び出し単位とその順序は維持された状態で出力されます。

    cout::operator<<() も同様。連続した operator<<() の呼び出しをまとめて一括処理したりすることはありませんが(それはスレッドセーフとは違う概念)、一つの呼び出しの中では破たんすることなくきちんと呼び出しとその後の状態変更を行うようになっています。

    また、メモリアロケートも同様です。malloc() や operator new() 関数などランタイム経由でメモリを借用する場合に、複数スレッドから同時に呼び出しが発生しても、重複したメモリ領域を貸し出したりしないということが行われています。もちろん、free/delete の時も同様です。

    こういったライブラリの「ある呼び出しが複数のスレッドから同時に行われた場合でも問題なく処理できる状態である」ことを、一般に「スレッドセーフ」「スレッド安全」などと呼びます。

    ちなみに...OpenMP は、スタートアップルーチンで、_beginthread 同様 ランタイムライブラリのスレッドスタートアップを呼び出します。

    ですので、少なくともランタイム(C/C++のレベル)においては、スレッドセーフな呼び出しはスレッドセーフを維持します。

    ただし、MFCについてはドキュメントを見たことがないのでわかりません。もしかしたらどこかにドキュメントがあるのかもしれませんが、見たことがないんですよねー。

    VS2010のPPL(OpenMPのようなコンパイラ拡張ではなく、ライブラリで実現した並列処理ライブラリ。TBB(Thread Building Block)のサブセット版のような感じのもの)も同様で、ランタイムレベルでは問題ありませんが、MFCなどをどう扱っているか?についてはわかりません。

    それと、CListBox::AddString() は、スタティックメソッドではありません。なので、どういう呼び出しをしているか?が重要です。

    スレッドセーフと、スレッド間のデータ共有は全く別の話ですし、ある変数への同時アクセスも別の問題です。

    これらは、非常に似通っていますが、解決策も視点も考慮すべき部分もすべて違います。

    並列処理は処理を複数同時に行うことで、総処理時間の見かけを短縮するためのテクノロジの「総称」です。

    この分野のうち、OpenMPは Concurrent(並行、同時実行)と呼ばれる部分を実現するためのテクノロジの一つとなります。

    ちなみに、一般的な意味でスレッドセーフを実現する部分は、parallel(並列)と呼ばれる部分の分野にあたります。

    OpenMPではなく、VS2010に搭載された同時実行ランタイムを土台にしたセッション資料でよければ、[勉強会]セッション資料公開 に公開しています。

    資料としてはあまり良いものではありませんが、並列処理を行ううえで、最低限知っておいてほしいことだけは載せてあるつもりです。


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/
    • 回答としてマーク Brillia 2011年10月27日 9:23
    2011年10月26日 1:58
  • SendMessage部分だけ少し補足。

    Windows はウィンドウあてメッセージの処理を、そのウィンドウが作られたスレッドの実行時に処理するように作られています。これを実現するのが通称メッセージポンプなどと呼ばれる、メッセージ処理ループです。

    ウィンドウを生成したスレッドとは別のスレッド(別プロセスを含む)から、メッセージをSendした場合、Windowsは、SendMessage 用の専用メッセージキューにメッセージをセットして、呼び出し元のスレッドを処理終了待機状態にします(仲澤さんの1の部分で、WaitForSingleObject( handle, INFINITE );を行っている状態になる)。

    2の部分は能動的に切り替えるわけではなく、OSのスケジュールで順番が来るのを待つだけですが、そのスレッドに作業時間順が回ってきて、PeekMessageまたはGetMessage が呼び出されると、先ほどセットしたメッセージを優先的に処理するようになっています。

    メッセージを処理する(DispatchMessageを呼び出す)と、中でそのウィンドウのプロシージャを呼び出し、しかるべき処理を行ってもらった後、その戻り値を、1の待機中のスレッドに返す形で、スレッドの待機処理を終了させます(3の部分。待機中のハンドルをシグナル状態に変更する)。

    シグナル状態になると、1の待機状態が解除されるので、SendMessage が終了して処理が継続される(4の部分)。

    という形になっています。

    OpenMPに限らずある瞬間に呼び出し元スレッドを止めて、一気に処理を行うような並列処理では、多くの場合その内部で別スレッドを起動して処理を行います。この時に、スレッド間で同期が走るような呼び出しを行うと、デッドロックを引き起こします。

    今回の場合、1の部分で待機に入っても、2に到達することがないため、デッドロックとなります。

    順序が担保されない並列処理で、AddString は意味をなさない気がするのですが?それとも順番がバラバラになるのを期待している?

    もしそうなのだとしたら、並列化している部分では、適当なバッファにデータを確保しておいて、処理終了後に一気にAddStringを繰り返すようにするか、そもそもその並列処理を行っている部分を別スレッドに切り出しておいて、メインスレッドは、そのスレッドを実行させたらすぐにメソッドから抜けてメッセージループに戻るようにしてやる必要があります。

    ちょっと違いますが、並列処理を行いながら、膨大な処理なのでプログレスバーを出したいような処理のための礎とできる記事を書いたので、こっちも貼っておきます。参考になるかはわかりませんけど... http://codezine.jp/article/detail/5352

     


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/
    • 回答としてマーク Brillia 2011年10月27日 9:22
    2011年10月26日 3:11
  • やや重箱の隅的補足をさせてもらうと、
    「CWinThread オブジェクトを使わずにスレッドを作成した
    マルチスレッド アプリケーションでは、・・・云々」
    の部分は、原文の英文を含めて、内容がやや「不親切」かもしれません。

    自分的にはAfxBeginThread()、又はclass CWinThreadを使用しない場合は
    そのスレッド関数以下ではMFCが使用できない、と書きたいところです。
    AfxBeginThread()の戻り値がCWinThread*なので問題ありませんけどね。

    また、CStringはMFCの中では比較的巨大なクラスでとても「程度」と言えるような
    簡素なしろものではないと考えます。さらに、_begintread()のスレッド関数内では
    CStringは一見まともに動いてますが、スレッド終了時に正しい後片付けができない、
    との認識ですであることも、付け加えておきます。


    2011年10月27日 5:09
  • 「#pragma omp parallel for」とはforの{・・・}内を1つのスレッドとして
    繰り返し回数分のスレッドを起動すると理解していますが、

    > 下記のコードで、CString s;をfor文の外に出して宣言すると、プロセス固まります。。。

    Azuleanさんも指摘していますが、上記の場合は複数のスレッドが同時に
    s.Format()を使おうとするようになりそうなので、論外ですね。

    次に{・・・}内でCListBox::AddString()を実行する件ですが、AddString()は、要は
     ::SendMessage( HWND, LB_ADDSTRING, 0, (LPARAM)文字列);
    なので、複数のスレッドから、唯一のリストボックスに対して上記を同時に実行するわけです。
    SendMessage()がスレッドを跨ぐ場合は
     1.呼び出し元のスレッドを停止。
     2.送信先のスレッドに切り替え
     3.コールバックを実行し結果を取得
     4.呼び出し元のスレッドを開始、結果を戻す
    となるはずなので、まぁやめといたほうが良いでしょう。

    さて、マルチスレッドについての最も重要な点は、
     1.スレッド化される関数が「リエントラント」である。
    という点ですね。つまり、その関数が途中まで実行されているときに、
    別のスレッドが、その関数を先頭から実行し始めても、両実行共、正常に完了することが、
    関数の構造上で、保障されていなければなりません。
    これば、スレッドの基礎的要件で、OpenMPでも同じだと考えられます。

    • 回答としてマーク Brillia 2011年10月27日 9:22
    2011年10月26日 2:16
  • MFCが使用できるにしても、同期を考える必要があるということですね。

    MFCを利用しているかと同期処理(同時アクセス制御)は関係ありません。利用しようとしているオブジェクトがMFCのものであるかそうでないかにかかわりなく、あるオブジェクトに同時に書き込み処理が行われる可能性があれば制御する必要があります。

    もしそうならば、 どの時点で、ブロック状態となったのでしょうか。

    2の部分は

    能動的に切り替えるわけではなく、OSのスケジュールで順番が来るのを待つだけですが、そのスレッドに作業時間順が回ってきて、PeekMessageまたはGetMessage が呼び出されると、先ほどセットしたメッセージを優先的に処理するようになっています。

    と、書いています。ですので、ブロックしているわけではないのですよ。単にメインスレッドでメッセージポンプが動くのを待っているだけにすぎないのです。

    結果的には並列処理を終えることができずに(並列化した部分でSendMessageが待機してしまうため)、処理が止まってしまいますが。。。

    それと、おまけになってしまいましたが、

    「Windowsはメッセージの処理をウィンドウを作成したスレッドで行います。ですので、SendMessage で呼び出す場合でも、同一スレッドから呼び出した場合は、メッセージキューを経由してスレッドを切り替えるわけではなく、同一スレッドなのでそのままプロシージャを呼び出します。」

    ということも覚えておいてください。

    このあたりは、そういうソースを書いてデバッグしてみるのが一番わかりやすいといえます。古いので良ければ、http://www.wankuma.com/seminar/20070721tokyo10/Default.aspx でこのメッセージ処理に関するネタでセッションやってるので参考にしてみてください。

    5年近く前のやつなので、恥ずかしい限りですが、内容そのものは今も陳腐化していない部分の話題です(あえてMFCなどのライブラリは利用せず生のAPIを利用した構造で表現しています)。

    並列処理ではないので、ちょっと違いますが、サブスレッドからメッセージを送った時の動きとかを実際に見せていますので、少しは参考になると思います。


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/
    • 回答としてマーク Brillia 2011年10月27日 9:24
    2011年10月26日 12:45
  • この内容からはまだつかめていないと言うことなのかな?

    • for ループの終了条件はすべての並列処理が終了したとき
    • 並列処理は SendMessage を呼び出しているのでメインスレッドが処理するまで待つ
    • すべての並列処理が終わるのを待っているときは、メッセージループが回らない(ブロック状態)

    これによって、お互いに待ち合う(デッドロック)状態になると。(裏付けとっていませんが)


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
    • 回答としてマーク Brillia 2011年10月27日 9:22
    2011年10月26日 16:02
    モデレータ

すべての返信

  • レス付きませんね。あんまり詳しくないですが、要はやってみれば良いのではないでしょうか。
    VS2008ではプロジェクトのオプションの「言語」の設定でOpenMP(2.0)が選択できます
    よね。そのプロジェクトで、並列にされる関数の中でCStringを使ってみればわかるのでは
    ないでしょうか。過去に_beginthreadで生成されたスレッド内でCStringを使用すると
    メモリーリークが発生した経験があります。OpenMPが_beginthreadしているかどうか、
    指標になるかもしれません。
    あんまり、役に立たなくてすみません。


    2011年10月25日 7:53
  • ご返事ありがとうございます。

    自分で検証したのですが、下記のコードは問題なく動作していますが、
    下記のコードで、for文の中でリストボックス にCListBox::AddStringしてみると、プロセスがデッドブロックした感じになります。

    おそらくCListBox::AddStringがマルチスレッド対応となっていないことが問題なのでしょうか?(MFCクラス全般?)
    じゃ、CStringList::AddTailもやばいんではないのかという疑問でてきて、でも正常に動作しているし。(たまたま?)

    ということで、なにかしら文献に記述しているものがあればと思っております。

    補足
    下記のコードで、CString s;をfor文の外に出して宣言すると、プロセス固まります。。。

    #ifdef _OPENMP
    #pragma omp parallel for
    #endif
     for(int i=0;i<1000;i++)
    {
       CString s;
       s.Format(_T("%2d"),i);
       lst.AddTail(s);
    }

    POSITION pos = lst.GetHeadPosition();
    while(pos)
    {
    m_lstListBox.AddString( lst.GetNext(pos));
    }

     


    • 編集済み Brillia 2011年10月25日 10:10
    2011年10月25日 10:04
  • わたしも詳しくはないですが、

    補足
    下記のコードで、CString s;をfor文の外に出して宣言すると、プロセス固まります。。。

    #ifdef _OPENMP
    #pragma omp parallel for
    #endif
     for(int i=0;i<1000;i++)
    {
       CString s;
       s.Format(_T("%2d"),i);
       lst.AddTail(s);
    }

    例コードで CString を for 文の外にだすのは問題がありますし、CListBox::AddString を複数スレッドから呼びだすのもダメでしょうから、OpenMP と MFC のどちらで固まってるのかなんとも言い難い状態ですね。

    OpenMP がメモリアクセスの同期処理を自動化してくれるとは思えないのですが・・・、デッドロックや現状が動いてるのなら同期とってくれるのかな?

    C言語系はスレッドセーフでなくMFCも同様なはずなので、スレッド間で参照するメモリ値更新には基本的に同期をとる必要があります。

    文献がということだったので軽く検索してみたのですが、文献は見つけられませんでした。

    役ただずの駄文返信で申し訳ありません・・・・。 

    2011年10月25日 11:09
  • MFCがスレッドセーフかどうか以前の問題だと思います。

    複数スレッドが同じオブジェクトを更新する場合には排他制御が必要です。

    「このライブラリはスレッドセーフです。」などという時の「スレッドセーフ」は、複数スレッドが同じオブジェクトを同時に更新しても大丈夫という意味ではないのでご注意を。

     


    2011年10月25日 13:12
  • おそらくCListBox::AddStringがマルチスレッド対応となっていないことが問題なのでしょうか?(MFCクラス全般?)

    MFC クラスというより、ウィンドウに対する操作はそのウィンドウが属するスレッドからのみ行うべきというのが Windows でのお作法です。
    それを OpenMP を使うことで破っているので、その結果にどうなるかは保障されないのはごく自然なことです。

    じゃ、CStringList::AddTailもやばいんではないのかという疑問でてきて、でも正常に動作しているし。(たまたま?)

    ウィンドウが絡んでいないので不安な面は一つ減りますが、どこかでメモリの再確保が走ると思います。
    それが複数スレッド同時に動いたりするとどうなるかわかりません。

    下記のコードで、CString s;をfor文の外に出して宣言すると、プロセス固まります。。。

    正直、その事例は何がしたいのかわかりません。OpenMP を使うメリットもないので…。

    とりあえず、MFC マルチスレッドあたりで調べてみるとよいのでは。
    http://msdn.microsoft.com/ja-jp/library/h14y172e.aspx


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
    2011年10月25日 13:43
    モデレータ
  • 横からの質問になっちゃって申し訳ありませんが、 ちちびんたリカさんが

    「このライブラリはスレッドセーフです。」などという時の「スレッドセーフ」は、複数スレッドが同じオブジェクトを同時に更新しても大丈夫という意味ではないのでご注意を。

    と述べてくれているのですが、私は「スレッドセーフ=複数スレッドからの操作が安全に行える」と認識していたのですが違うのでしょうか?

    マルチスレッドのコーディングするときに排他制御?な記述は基本的に行っていたので、今一つ「スレッドセーフ」という用語の意味を取り間違えてるかな?と常々思っていたので気になります。

    よろしければぜひとも御教授願いたいです。

     

    2011年10月25日 15:04
  • 各ライブラリによって少々異なるでしょうが、だいたい以下のような当然のようなことしか安全とは言っていないと思います。


    (1)複数のスレッドから同一のオブジェクトへ同時にリードアクセスするのは安全

    (2)(スレッド間で共有していない)異なるオブジェクトへ同時にリードorライトアクセスするのは安全

     

    つまり、リードアクセスは排他制御しなくても安全だけど、ライトアクセスは排他制御する必要があるってことです。

     

    2011年10月25日 16:13
  • 一般的にライブラリがスレッドセーフという場合、ある関数やメソッドなどへのアクセスを複数のスレッドで同時に行っても破たんしないということを指します。

    例えば、コンソールへの画面出力。ちょっと前にあちこちで話題になっていましたけどw

    puts( "thread 1-call1" );
    puts( "thread 1-call2" );

    という呼び出しを行うスレッドと

    puts( "thread 2-call1" );
    puts( "thread 2-call2" );

    という呼び出しを行うスレッドがあったとした場合。

    これらが本当に同時にアクセスが入ったとしても、

    tthrehradead 21-ccaall12

    などとわけのわからない状況になったりせず、

    thread 1-call1
    thread 2-call1
    thread 1-call2
    thread 2-call2

    と、少なくとも呼び出し単位とその順序は維持された状態で出力されます。

    cout::operator<<() も同様。連続した operator<<() の呼び出しをまとめて一括処理したりすることはありませんが(それはスレッドセーフとは違う概念)、一つの呼び出しの中では破たんすることなくきちんと呼び出しとその後の状態変更を行うようになっています。

    また、メモリアロケートも同様です。malloc() や operator new() 関数などランタイム経由でメモリを借用する場合に、複数スレッドから同時に呼び出しが発生しても、重複したメモリ領域を貸し出したりしないということが行われています。もちろん、free/delete の時も同様です。

    こういったライブラリの「ある呼び出しが複数のスレッドから同時に行われた場合でも問題なく処理できる状態である」ことを、一般に「スレッドセーフ」「スレッド安全」などと呼びます。

    ちなみに...OpenMP は、スタートアップルーチンで、_beginthread 同様 ランタイムライブラリのスレッドスタートアップを呼び出します。

    ですので、少なくともランタイム(C/C++のレベル)においては、スレッドセーフな呼び出しはスレッドセーフを維持します。

    ただし、MFCについてはドキュメントを見たことがないのでわかりません。もしかしたらどこかにドキュメントがあるのかもしれませんが、見たことがないんですよねー。

    VS2010のPPL(OpenMPのようなコンパイラ拡張ではなく、ライブラリで実現した並列処理ライブラリ。TBB(Thread Building Block)のサブセット版のような感じのもの)も同様で、ランタイムレベルでは問題ありませんが、MFCなどをどう扱っているか?についてはわかりません。

    それと、CListBox::AddString() は、スタティックメソッドではありません。なので、どういう呼び出しをしているか?が重要です。

    スレッドセーフと、スレッド間のデータ共有は全く別の話ですし、ある変数への同時アクセスも別の問題です。

    これらは、非常に似通っていますが、解決策も視点も考慮すべき部分もすべて違います。

    並列処理は処理を複数同時に行うことで、総処理時間の見かけを短縮するためのテクノロジの「総称」です。

    この分野のうち、OpenMPは Concurrent(並行、同時実行)と呼ばれる部分を実現するためのテクノロジの一つとなります。

    ちなみに、一般的な意味でスレッドセーフを実現する部分は、parallel(並列)と呼ばれる部分の分野にあたります。

    OpenMPではなく、VS2010に搭載された同時実行ランタイムを土台にしたセッション資料でよければ、[勉強会]セッション資料公開 に公開しています。

    資料としてはあまり良いものではありませんが、並列処理を行ううえで、最低限知っておいてほしいことだけは載せてあるつもりです。


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/
    • 回答としてマーク Brillia 2011年10月27日 9:23
    2011年10月26日 1:58
  • 「#pragma omp parallel for」とはforの{・・・}内を1つのスレッドとして
    繰り返し回数分のスレッドを起動すると理解していますが、

    > 下記のコードで、CString s;をfor文の外に出して宣言すると、プロセス固まります。。。

    Azuleanさんも指摘していますが、上記の場合は複数のスレッドが同時に
    s.Format()を使おうとするようになりそうなので、論外ですね。

    次に{・・・}内でCListBox::AddString()を実行する件ですが、AddString()は、要は
     ::SendMessage( HWND, LB_ADDSTRING, 0, (LPARAM)文字列);
    なので、複数のスレッドから、唯一のリストボックスに対して上記を同時に実行するわけです。
    SendMessage()がスレッドを跨ぐ場合は
     1.呼び出し元のスレッドを停止。
     2.送信先のスレッドに切り替え
     3.コールバックを実行し結果を取得
     4.呼び出し元のスレッドを開始、結果を戻す
    となるはずなので、まぁやめといたほうが良いでしょう。

    さて、マルチスレッドについての最も重要な点は、
     1.スレッド化される関数が「リエントラント」である。
    という点ですね。つまり、その関数が途中まで実行されているときに、
    別のスレッドが、その関数を先頭から実行し始めても、両実行共、正常に完了することが、
    関数の構造上で、保障されていなければなりません。
    これば、スレッドの基礎的要件で、OpenMPでも同じだと考えられます。

    • 回答としてマーク Brillia 2011年10月27日 9:22
    2011年10月26日 2:16
  • SendMessage部分だけ少し補足。

    Windows はウィンドウあてメッセージの処理を、そのウィンドウが作られたスレッドの実行時に処理するように作られています。これを実現するのが通称メッセージポンプなどと呼ばれる、メッセージ処理ループです。

    ウィンドウを生成したスレッドとは別のスレッド(別プロセスを含む)から、メッセージをSendした場合、Windowsは、SendMessage 用の専用メッセージキューにメッセージをセットして、呼び出し元のスレッドを処理終了待機状態にします(仲澤さんの1の部分で、WaitForSingleObject( handle, INFINITE );を行っている状態になる)。

    2の部分は能動的に切り替えるわけではなく、OSのスケジュールで順番が来るのを待つだけですが、そのスレッドに作業時間順が回ってきて、PeekMessageまたはGetMessage が呼び出されると、先ほどセットしたメッセージを優先的に処理するようになっています。

    メッセージを処理する(DispatchMessageを呼び出す)と、中でそのウィンドウのプロシージャを呼び出し、しかるべき処理を行ってもらった後、その戻り値を、1の待機中のスレッドに返す形で、スレッドの待機処理を終了させます(3の部分。待機中のハンドルをシグナル状態に変更する)。

    シグナル状態になると、1の待機状態が解除されるので、SendMessage が終了して処理が継続される(4の部分)。

    という形になっています。

    OpenMPに限らずある瞬間に呼び出し元スレッドを止めて、一気に処理を行うような並列処理では、多くの場合その内部で別スレッドを起動して処理を行います。この時に、スレッド間で同期が走るような呼び出しを行うと、デッドロックを引き起こします。

    今回の場合、1の部分で待機に入っても、2に到達することがないため、デッドロックとなります。

    順序が担保されない並列処理で、AddString は意味をなさない気がするのですが?それとも順番がバラバラになるのを期待している?

    もしそうなのだとしたら、並列化している部分では、適当なバッファにデータを確保しておいて、処理終了後に一気にAddStringを繰り返すようにするか、そもそもその並列処理を行っている部分を別スレッドに切り出しておいて、メインスレッドは、そのスレッドを実行させたらすぐにメソッドから抜けてメッセージループに戻るようにしてやる必要があります。

    ちょっと違いますが、並列処理を行いながら、膨大な処理なのでプログレスバーを出したいような処理のための礎とできる記事を書いたので、こっちも貼っておきます。参考になるかはわかりませんけど... http://codezine.jp/article/detail/5352

     


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/
    • 回答としてマーク Brillia 2011年10月27日 9:22
    2011年10月26日 3:11
  • とっちゃん様、詳しい解説をありがとうございます。

    また、メモリアロケートも同様です。malloc() や operator new() 関数などランタイム経由でメモリを借用する場合に、複数スレッドから同時に呼び出しが発生しても、重複したメモリ領域を貸し出したりしないということが行われています。もちろん、free/delete の時も同様です。

    こういったライブラリの「ある呼び出しが複数のスレッドから同時に行われた場合でも問題なく処理できる状態である」ことを、一般に「スレッドセーフ」「スレッド安全」などと呼びます。

    まさにこのあたりでメモリアロケータはマルチスレッドの場合どうなるんだ?

    「スレッドセーフ」を取り違えてる?って長年ひっかっておりました。

    CRT もスレッドセーフな呼出しがかなり含まれると思っていれば、とりあえずは間違ってないかな・・?

    資料の方もありがたく参照させていただきます。

     

    ちちびんたリカさんも返信ありがとうございました。

     

    Brillia さん本題と違う内容の質問しちゃって申し訳ございませんでした。

    2011年10月26日 3:33
  • なるほど、

    今回の場合、1の部分で待機に入っても、2に到達することがないため、デッドロックとなります。

    メッセージを処理するスレッドが SendMessage で待機状態になっているため、2の処理が行われることがない。

    という状態になってるんですね。

    経験上固まると思っても、ここまでは読取れませんでした。もっと勉強せんといけませんね・・・・。

    例示コードに意図が見えないのは「動作勉強用テストコード」と自分は判断してましたがどうなのでしょうね?

    修正:SendMessage ではなく parallel for の完了待ちでが正解ですかね・・・。
    • 編集済み kyano30 2011年10月26日 8:22 間違ってる?
    2011年10月26日 8:16
  • 皆様、ご返事ありがとうございます。

    MFCが使用できるにしても、同期を考える必要があるということですね。

    順序が担保されない並列処理で、AddString は意味をなさない気がするのですが?それとも順番がバラバラになるのを期待している?

    CListBox::AddStringは、デバック用に結果を見たかっただけで、意味はないです。
    提示したコードでは、CStringListをバッファ変数として、各スレッドがリストに挿入してくれましたが、
    CStringListに関しても 「スレッドセーフ」と記載していないので、やっぱりまずいんだろうな。。。
    この辺は、もっと研究します。ありがとうございました。 


     とっちゃん様の、SendMessageの説明で疑問があるのですが、ご教授願いますか。

    今回の場合、1の部分で待機に入っても、2に到達することがないため、デッドロックとなります。

    これは、Listコントロールが所属しているスレッドが、ブロック状態となっているから、2に到達できないことになっているのでしょうか。
    もしそうならば、 どの時点で、ブロック状態となったのでしょうか。

    たとえば、メインスレッドがワーカスレッドを起動し、メインスレッドはワーカスレッドが終了するまで待機状態にする。
    ワーカスレッドは、メインスレッドに所属しているコントロールにSendMessageをおこなう。
    メインスレッドは、ブロック状態の為、SendMessageが処理できない。
    ワーカスレッドは、SendMessageが処理されるまで待機状態となる。

    つまりデッドロックになる。。。

    しかし、今回の場合、メインスレッドが待機状態となる箇所があるのでしょうか。 

    もしかして、OpenMPが実行中は、呼び出し元のスレッドは待機状態になっているからですか。

     

     

    2011年10月26日 8:27
  • MFCが使用できるにしても、同期を考える必要があるということですね。

    MFCを利用しているかと同期処理(同時アクセス制御)は関係ありません。利用しようとしているオブジェクトがMFCのものであるかそうでないかにかかわりなく、あるオブジェクトに同時に書き込み処理が行われる可能性があれば制御する必要があります。

    もしそうならば、 どの時点で、ブロック状態となったのでしょうか。

    2の部分は

    能動的に切り替えるわけではなく、OSのスケジュールで順番が来るのを待つだけですが、そのスレッドに作業時間順が回ってきて、PeekMessageまたはGetMessage が呼び出されると、先ほどセットしたメッセージを優先的に処理するようになっています。

    と、書いています。ですので、ブロックしているわけではないのですよ。単にメインスレッドでメッセージポンプが動くのを待っているだけにすぎないのです。

    結果的には並列処理を終えることができずに(並列化した部分でSendMessageが待機してしまうため)、処理が止まってしまいますが。。。

    それと、おまけになってしまいましたが、

    「Windowsはメッセージの処理をウィンドウを作成したスレッドで行います。ですので、SendMessage で呼び出す場合でも、同一スレッドから呼び出した場合は、メッセージキューを経由してスレッドを切り替えるわけではなく、同一スレッドなのでそのままプロシージャを呼び出します。」

    ということも覚えておいてください。

    このあたりは、そういうソースを書いてデバッグしてみるのが一番わかりやすいといえます。古いので良ければ、http://www.wankuma.com/seminar/20070721tokyo10/Default.aspx でこのメッセージ処理に関するネタでセッションやってるので参考にしてみてください。

    5年近く前のやつなので、恥ずかしい限りですが、内容そのものは今も陳腐化していない部分の話題です(あえてMFCなどのライブラリは利用せず生のAPIを利用した構造で表現しています)。

    並列処理ではないので、ちょっと違いますが、サブスレッドからメッセージを送った時の動きとかを実際に見せていますので、少しは参考になると思います。


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/
    • 回答としてマーク Brillia 2011年10月27日 9:24
    2011年10月26日 12:45
  • ご返事ありがとうございます。

    先走った質問すみませんでした。

    どこにもブロック状態になっているなんか記述されていないのに
    自分の知識レベルで物事を解釈してしました。

    私の知識では、メインスレッドがブロック状態ではなくメッセージポンプは動作しておれば
    処理されて問題ないようにみえてしまいます。

    複数のワーカースレッドから複数のSendMessageが実行されるところに問題があるように
    みえますが、思い込まずに

    「Windows メッセージを使いこなす-Windows 流オブジェクト指向-」

    でまずは勉強させていただきます。

     

    2011年10月26日 14:35
  • この内容からはまだつかめていないと言うことなのかな?

    • for ループの終了条件はすべての並列処理が終了したとき
    • 並列処理は SendMessage を呼び出しているのでメインスレッドが処理するまで待つ
    • すべての並列処理が終わるのを待っているときは、メッセージループが回らない(ブロック状態)

    これによって、お互いに待ち合う(デッドロック)状態になると。(裏付けとっていませんが)


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
    • 回答としてマーク Brillia 2011年10月27日 9:22
    2011年10月26日 16:02
    モデレータ
  • >私の知識では、メインスレッドがブロック状態ではなくメッセージポンプは動作しておれば
    処理されて問題ないようにみえてしまいます。

    これは、Spy++を使用すれば検証できるかもしれませんね(OSが健全なら)。

    さて、OpenMPは生成したスレッド群が安全に共通データにアクセスすることを
    目指しているらしいので、そのための手段もいろいろと用意されているようです。
    特に同期オブジェクトを意識的にコードしなくて済むようなので、その辺から
    攻めてみるのも楽しそうですね。
    まぁ、SendMessage()はだめっぽい気がしますけど(vv;)。

    2011年10月27日 2:17
  • 一応、マルチスレッド: プログラミングのヒントのページに

    CWinThread オブジェクトを使わずにスレッドを作成したマルチスレッド アプリケーションでは、作成したスレッドから MFC オブジェクトにアクセスできません。

    という厳しい記述があります。同等の記述がCRT; Cランタイムライブラリにもあるかどうか探しましたが見つかりませんでした。
    # _beginthread()にその記述がないかを当たり、そこからリンクされていたマルチスレッドを参照しました。

    保証する/しないで言えば保証されないでしょうが、できるできないで言えば曖昧で、例えばCString程度なら動いてしまうのではとは思います。もちろん、議論されているようにマルチスレッドに絡む排他は必須です。これも同じくプログラミングのヒントのページから

    サイズおよびパフォーマンス上の理由により、MFC オブジェクトのスレッドの安全性は、オブジェクト レベルでは保証されず、クラス レベルでしか保証されません。 つまり、2 つの異なるスレッドで 2 つの異なる CString オブジェクトを操作することはできますが、2 つの異なるスレッドで同じ CString オブジェクトを操作することはできません。
    2011年10月27日 2:48
  • やや重箱の隅的補足をさせてもらうと、
    「CWinThread オブジェクトを使わずにスレッドを作成した
    マルチスレッド アプリケーションでは、・・・云々」
    の部分は、原文の英文を含めて、内容がやや「不親切」かもしれません。

    自分的にはAfxBeginThread()、又はclass CWinThreadを使用しない場合は
    そのスレッド関数以下ではMFCが使用できない、と書きたいところです。
    AfxBeginThread()の戻り値がCWinThread*なので問題ありませんけどね。

    また、CStringはMFCの中では比較的巨大なクラスでとても「程度」と言えるような
    簡素なしろものではないと考えます。さらに、_begintread()のスレッド関数内では
    CStringは一見まともに動いてますが、スレッド終了時に正しい後片付けができない、
    との認識ですであることも、付け加えておきます。


    2011年10月27日 5:09
  • 皆様、ありがとうござました。

    Azulean様の説明により、ようやくデッドロック状況理解できました。
    おはずかしいです。

    またOpenMPとMFCに関しては、今回明確にわかった結果、メッセージが絡むクラスの使用に問題があることを考えると
    (実際は、CStringList、CStringでも問題がでるのかもしれませんが、とりあえずうまくいってるのですが、たまたまのかもしれませんし・・・)

    なにか文献がでてくるまでは、OpenMP内ではC++のプリミティブ変数でバッファを作成して使用し 、
    バッファ(結果) をMFCのクラスに戻すという方法で実際に効果が出るのかを判断して、使用するかどうかを検討したと思います。

     

     

     

     

     

    2011年10月27日 9:22
  • 長年 CRT はスレッドセーフではないという勘違いをしていた馬鹿の戯言になりますが・・・。

     

    CString が _beginthread() のスレッド内でメモリリーク?になるというのはバグのような気がします。

    ATL/MFC の共有クラス

    に VC++ .NET 2002 以降から CString 自体は ATL/MFC の共有クラスになり「C++ ネイティブ プロジェクト」で利用できる。

    CString の使用

    に「C++ での一般的な開発に使用できる 」との記述なので Microsoft 的には利用できるようにしてると思われます。

     よってリークするならライブラリとしてのバグに当たりそうな気がしますね。

     

    CWinThread は単純な API のラッパーでなく色々と内部管理してたので、MFC でマルチスレッドなら CWinThread を使うべきでしょうけど・・・。 

     

    2011年10月27日 9:25
  • # 何のためにシングルスレッド版とマルチスレッド版が存在していたのやら…。スレッドセーフではないつくりならシングルスレッドのみでいいはずです(’’*

    CStringが_beginthread()のスレッドでメモリリークになるという話は気になります。具体的にはどういう条件なのでしょうね。メモリマネージャのバグ?

    2011年10月28日 3:59
  • 自分としてはMFCの「仕様」と理解しています。バグとの認識はありません。
    メモリーリークは簡単に再現できます。

    1.メインフレームに「Threadの起動」メニューを追加、ハンドラも追加
    2.そのハンドラで次のコードを実行する
      void MainFrame::OnThread(){
          CC  cc;
          ::_beginthread( thFunc, 0, PVOID( &cc));
      }
    3.クラスCCは次のコード
      class CC{
      public:
          CStringA  m_Str;
      };
    4.thFuncは次のコード
      void thFunc( void* p){
          CC * cc = ( CC *)p;
          cc->m_Str = "0123456789";
          _endthread();
      }
    テストアプリケーションを実行したら、メニューから「Threadの起動」を選択
    すぐにアプリケーションを終了する。デバッグビルドなら次のリークが報告されます。

    Detected memory leaks!
    Dumping objects ->
    f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\strcore.cpp(141) : {2326} normal block at 0x00E22A10, 27 bytes long.
     Data: <   x            > 0C 00 E5 78 0A 00 00 00 0A 00 00 00 01 00 00 00
    Object dump complete.
    プログラム '[5892] Thread_Test.exe: ネイティブ' はコード 0 (0x0) で終了しました。

    ただし、VS2008 on XP(+SP3)の場合ですね。2003でもおんなじだったと記憶してます。
    また、複数回このスレッドを実行すると、当然ながら free に失敗します。
    もちろんAfxBeginThreadを使えばこの様なリークは発生しません。
    まぁ「一般的に利用できる」状態とは言いかねますです(vv;)。

    2011年10月31日 7:28
  • 確認していませんが、thFunc() で _endthread() を呼ばなければリークしなくなるということはありませんか?

    あれ?あれ?これ、サンプルソースだから簡素化してると思いますが、これだとオブジェクトが破壊されちゃってませんか?

    このコードだと、OnThread の終了前に thFunc が実行されることが保証されてないです。

    なので、スタックにある CCクラスのインスタンスがデストラクトされてしまいませんか?おそらくその結果としてメモリーリークしているのだと思います。

    これは、MFCじゃなくてもリークしてしまう。。。というか動いたらラッキーなコード。。。

     

     


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/
    2011年10月31日 8:41
  • _beginthread()呼び出しの後の処理順序がどうなっているかですよね。

    1. 元のスレッドでは_beginthread()から制御が戻り、OnThread()を抜けようとし、その際にccのデストラクターが呼ばれる
    2. 新規に作成されたスレッドではtheFunc()の実行が始まる

    2.よりも1.が先に実行されてしまった場合、ccはデストラクター実行済みですから、変な状態になるのは当然です。

    とっちゃんさんへ:
    theFunc()の方はクラスインスタンスへのポインターなのでデストラクターは実行されませんよん。 

    2011年10月31日 8:55
  • 失礼しました。このコードぢゃ実証になりませんねぇ  orz.
    不正確な情報を流してしまって申し訳ありません。
    リークが発生したコードはCWndが絡むので、いろいろ省いたんですが、
    やりすぎました。ちなみに、CC cc;をアプリケーションのグローバルにすると、
    リークは発生しませんね。もう一回コードを確認してみます(vv;)。


    2011年10月31日 9:09
  • 例示コードの修正で現象発生しないとなると条件特定は難しそうですかね。

     

    不正確な情報を流した自分がいうのもなんですけど、「経験した情報」を流していただけるのは非常にありがたいです。

     

    佐祐理様 #CRTのシングル/マルチスレッド版を知っていながら気付けなかった間抜けでした orz。

    とっちゃん様 #資料、楽しませていただきました。

     

    2011年10月31日 12:38