none
timeSetEvent関数によるタイマー処理とメインスレッドの処理の関係について RRS feed

  • 質問

  • お世話になります、鏑木です。
    以前のスレッドでも似たような質問をさせて頂いたのですが、新しいスレッドにして、もっと
    具体的に意見をお聞きしたいと思い質問させていただきました。

    現在作成中のアプリケーションの仕様としては、ダイアログベースのアプリケーション立ち上げ時に
    メインダイアログのOnInitDialog内にてtimeSetEvent関数を呼び出し、タイマー処理をしています。
    タイマーは500msec毎に呼び出され、そこでイベントをシグナル状態にして、別スレッドでイベントシグナル
    待機中の処理をさせようと思っております。

    そこでアプリケーションデバック時に、メインダイアログ上のボタン等を選択して、他のダイアログを呼び出していると、
    常にではないのですが、たまに別スレッドの処理が500msecで処理されない場合があります。
    デバックで確かめてみると、timeSetEventによるタイマー処理まで500msecで呼び出されているのですが、タイマー
    処理ないでイベントをシグナル状態とする作業で、普段なら5msecくらいしかかからないはずが、80msecかかっていたり
    する場合がありました。
    この原因について考え付くことがありましたら、ご意見を頂ければなと思っております。

    尚簡単にコードをかくと・・
    //メインダイアログのOnInitDialogで
    BOOL C****Dlg::OnInitDialog()
    {
     //処理待ちイベント
     Wait_Event = CreateEvent(NULL,true,false,NULL);
     //処理用スレッド
     AfxBeginThread(Func_Thread,this);
     //タイマーイベント
     ::timeSetEvent(500,timercaps.wPeriodMin,Func,reinterpret_cast<DWORD_PTR>(this),TIME_PERIODIC);

    }

    //タイマーイベント
    void CALLBACK C***Dlg::Func(UINT uTimerID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2)
    {
      SetEvent(Wait_Event);
    }

    UINT Func_Thread(LPVOID lParam)
    {
       while(1){
         WaitForSingleObject(Wait_Event,INFINITE);
        //処理
        ResetEvent(Wait_Event);
       }
    }

    となっております。おおざっぱなコードなので説明のない変数
    がまじってますが、ご了承ください。

    2010年2月26日 8:29

すべての返信

  • いまいちわからないのですが、

    1.手動でイベントを発生させてから、スレッドの
      WaitForSingleObject()がシグナルになるまでの時間が5~80[ms]に
      変動するということでしょうか。

    2.それとも、MMTimerの500[ms]のインターバルが変動するのでしょうか。

    2.の場合はtimeGetDevCaps()で取得できるタイマーの分解能や、
    どっかでやっているだろうtimeBeginPeriod()での設定値はど~なっているのでしょう。

    2010年2月26日 8:54
  • なにやらデータ受信タイマーのみが作成されていますが、同時に描画用のタイマーも起動している、という認識でいいでしょうか?

    また、以前のスレッドで yominet さんが指摘しておられた、他のダイアログの生成方法がどうなっているのか、も気になります。

    他のダイアログの表示の為に、CPU に負荷がかかり、全てのスレッドが遅くなる、という現象が起きているのかもしれません。もし、他のダイアログというのが、全てモードレスであれば、メインダイアログの作成時に、非表示状態で他のダイアログも作成して、ShowWindow で表示 / 非表示を切り替えた方が実行時の負荷は起動時に集中する為、以降の処理が比較的スムーズに行くのでは、と思います。

    個人的には、タイマーは一つで描画用 50 ms 程度、起動時に timeGetTime または GetTickCount で現在の時間を取得し、その値を元にイベント処理も行う方がいいと思っています。


    BOOL C****Dlg::OnInitDialog()
    {
        // 他のダイアログを作成
        m_dlg1.Create(IDD_OTHER1, this); // テンプレートには WS_VISIBLE を含めない事
        m_dlg2.Create(IDD_OTHER2, this);
        m_dlg3.Create(IDD_OTHER3, this);
        m_dlg4.Create(IDD_OTHER4, this);

        //処理待ちイベント
        Wait_Event = ::CreateEvent(NULL, TRUE, FALSE, NULL);

        //処理用スレッド
        AfxBeginThread(Func_Thread, this);

        // 現時刻取得
        m_dwTime = ::timeGetTime(); or ::GetTickCount();

        // 経過時間初期化
        m_dwCounter = 0L;

        //タイマーイベント
        ::timeSetEvent(50,timercaps.wPeriodMin,Func,reinterpret_cast<DWORD_PTR>(this),TIME_PERIODIC);
    }

    //タイマーイベント
    void CALLBACK C***Dlg::Func(UINT uTimerID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2)
    {
        C***Dlg* pDlg = reinterpret_cast<C***Dlg*>(dwUser);

        // 現時刻を取得
        DWORD dwCurrent = ::timeGetTime(); or ::GetTickCount();

        // OS 起動時より DWORD 時間が経過していないか確認 ( 不要かも知れません )
        if (pDlg->m_dwTime > dwCurrent)
        {
            dwCurrent = ULONG_MAX - pDlg->m_dwTime + dwCurrent;
        }

        // 経過時間を更新
        pDlg->m_dwCounter += dwCurrent;

        if (500L <= pDlg->m_dwCounter)
        {
            // 500 ms 経過しているのでイベントをシグナルに
            SetEvent(pDlg->Wait_Event);

            // カウンタを更新
            pDlg->m_dwCounter -= 500L;
        }

        // 現時刻を更新
        pDlg->m_dwTime = dwCurrent;

        // 描画処理
        pDlg->Invalidate(); // ???
    }

    UINT Func_Thread(LPVOID lParam)
    {
        C***Dlg* pDlg = reinterpret_cast<C***Dlg*>(lParam);

        while(1)
        {
            ::WaitForSingleObject(Wait_Event,INFINITE);

            // 処理
            ::ResetEvent(Wait_Event);

            // カウンタを更新
    //     pDlg->m_dwCounter -= 500L;
       }
    }


    とまあ、こういう感じで一つのタイマーで描画処理とデータ取得処理を行うのがいいかと。

    TimerProc 内と Func_Thread 内の両方で カウンタを更新していますが、実際はどちらかを選んで下さい。多分、Func_Thread 内で更新した方が規定量のデータを読み込めなかった時の対処が容易になると思いますが、Func_Thread 内の処理中に TimeProc が呼び出される可能性があるので、排他処理が必要となるでしょう。

    後は、仲澤@失業者 さんが指摘しておられる、どのタイミングで何が遅くなるのか、と、timeBeginPeriod の設定値をどうしているか、ですね。

    • 編集済み ミッヒー 2010年2月26日 11:49 ; が抜けていたのと、API の選択を / から or に変えました。たびたびすいません・・・。
    2010年2月26日 10:00
  • 仲澤@失業者様、ご回答ありがとうございます。

    マルチメディアタイマーのインターバル自体は変わらないようなのですが、WaitForSingleObjectが
    シグナル状態になるまでの時間が変動してしまいます。つまり、仲澤様のおっしゃる1です。

    ちなみにtimeBegiPeriodによる設定値は1としています。

    現状はtimeSetEventのインターバル時間を20msecと、かなり小さく設定しているのですが、
    timeSetEventで20というのはあまり小さい値で、設定してはいけないものなのでしょうか?
    timeSetEventから呼び出されるCALLBACK関数が呼び出される時間を調べてみると、
    20msecごとではなく、約17msec毎で、たまに4msecで呼ばれる時もあります。
    2010年3月1日 2:20
  • ミッヒー様、いつもご回答ありがとうございます。

    質問を簡単にするために、波形描画タイマー部分を省いて、データ受信タイマーのみとして質問させていただきました。
    説明不足で申し訳ございませんでした。

    モーダレスダイアログの作成については、ミッヒー様のおっしゃるように、初めに、各ダイアログを作成してから非表示としております。
    モーダレスで作成したダイアログを呼び出すときは、非表示のダイアログを表示させるという処理のみをしているのですが、やはり
    タイマーのインターバルに影響があるようです。
    yominet様のご指摘とおりに、何もコントロールやビットマップを張り付けていないダイアログを作成して、呼び出してみたのですが、
    それでもタイマーの時間に影響が及ぼされます。
    処理方法としては現状も、ミッヒー様のご説明に似た形となっているのですが、肝心のタイマーイベントの処理が、目的のタイマーインターバル
    で呼ばれていない為、正常に動作させることが出来ない状態になっております。

    仲澤@失業者様のご回答にも書かせていただいたのですが、timeSetEventのタイマー処理の呼び出し時間を調べてみたら、目的通りに
    のインターバルで呼び出されておらず、20msecという設定値が難しいのかなと思っております。
    しかし試しにタイマーインターバルを100msec等に変更しても、他ダイアログを呼び出した際にはそのタイマーインターバルが変動してしまいます。。。
    2010年3月1日 2:27
  • WaitForSingleObjectが
    シグナル状態になるまでの時間が変動してしまいます。つまり、仲澤様のおっしゃる1です。
    時間かがかかりそうな
     A.描画処理
     B.受信処理
    のどちらか、又は両方をしない場合でも、そうなりますか。
    そうであれば外乱(他アプリやOS)です。そうでない場合は自明ですよね。

    20[ms]のインターバルですが、前発言で質問した
     1.timeGetDevCaps()で取得したタイマーの分解能は1[ms]だった。
     2.timeBeginPeriod()の戻り値はTIMERR_NOERRORだった。
    は確認しましたか(再質問)。

    2010年3月1日 6:49
  • 仲澤@失業者様、ご回答ありがとうございます。

    先ほどの質問にお答えしておらず、申し訳ございませんでした。

    >1.timeGetDevCaps()で取得したタイマーの分解能は1[ms]だった。
    分解能は1[ms]を設定しております。

    >2.timeBeginPeriod()の戻り値は TIMERR_NOERRORだった。
    戻り値はTIMERR_NOERRORでした。



    >時間かがかかりそうな
    > A.描画処理
    > B.受信処理
    >のどちらか、又は両方をしない場合でも、そうなりますか。
    >そうであれ ば外乱(他アプリやOS)です。そうでない場合は自明ですよね。
    描画処理、受信処理を全てはずし、timeSetEventによるタイマーのみにしてアプリを起動させ、
    その状態で他ダイアログを呼び出してみましたがやはり同じように、たまにタイマーインターバルが変動するようです。
    常に変動するのであれば、あきらめるのですが、たまに、とういうことが何かしらの条件があるのかなと思えてならないです。。
    2010年3月1日 8:18
  • 確認不足でした。

    WinCE の TimeProc に関するドキュメントは見付けられませんでしたが、Windows の TimeProc のドキュメント

    http://msdn.microsoft.com/en-us/library/dd757631(VS.85).aspx

    には

    Applications should not call any system-defined functions from inside a callback function, except for PostMessage, timeGetSystemTime, timeGetTime, timeSetEvent, timeKillEvent, midiOutShortMsg, midiOutLongMsg, and OutputDebugString.
    とあります。timeSetEvent の TimeProc 内で SetEvent を呼び出すのが正しいのか、という疑問が。

    ::SetEvent ではなく、::timeSetEvent(0, 0, Wait_Event, 0L, TIME_CALLBACK_EVENT_SET) とするべきに思えてきました。

    ただ、この場合、timeKillEvent を何時呼び出せばいいのか、よく判りません。

    MMRESULT mmTimerID = ::timeSetEvent(0, 0, Wait_Event, 0L, TIME_CALLBACK_EVENT_SET | TIME_KILL_SYNCHRONOUS);
    ::timeKillEvent(mmTimerID);

    とする事で、timeKillEvent と同時にイベントがシグナル状態になるのかもしれませんが、試していません。

    別手段として、ユーザー定義メッセージをメインダイアログ ( メインスレッド ) に PostMessage で送り、その中で SetEvent を呼び出すべきかもしれませんが、SendMessage では無い分、遅延が生じそうな感じもします。


    また、TimeProc の中から SetEvent を呼び出すのが正しい ( というよりは WinCE での TimeProc 内での禁止事項として明記されていない ) 場合ですが、データ受信スレッドは WaitForSingleObject で停止しているのでしょうか?

    データの受信に時間が掛かり、イベントはシグナル状態になっているのに、スレッドが処理中で、処理終了後、WaitForSingleObject 呼び出しと同時に処理が始まる、という事は無いでしょうか?

    イベントをシグナルにする直前と、データ受信スレッドの処理完了時刻を timeGetTime で取得し、データ受信が 0.5 秒以内に確実に終わっているかの検証も必要かと思います。

    0.5 秒以内に終わっていない場合があったなら・・・データ受信スレッドとイベントを 2 つずつ作成して、交互にデータ受信をさせる事になるでしょうか。
    • 編集済み ミッヒー 2010年3月1日 11:44 誤記訂正
    2010年3月1日 11:36
  • ミッヒー様、ご回答ありがとうございます。
    Applications should not call any system-defined functions from inside a callback function, except for PostMessage , timeGetSystemTime , timeGetTime , timeSetEvent , timeKillEvent , midiOutShortMsg , midiOutLongMsg , and OutputDebugString .
    >とあります。timeSetEvent の TimeProc 内で SetEvent を呼び出すのが正しいのか、という疑問が。
    確かにです。timeSetEventのCALLBACKにはあまり負荷のかからない処理をしなければならないらしく、呼び出せる関数に制限がかかっているということは存じておりました。
    そこが原因なのかなとも思ったのですが。。
    しかし、マルチメディアタイマーの処理で、描画処理、データ受信処理を省いて、ただタイマーのみを動かした状態で、他ダイアログを呼び出してみたところ、タイマーインターバルが変動
    してしまうことから、関数の呼び出しも関係はあるかもしれないのですが、もっとタイマーに問題があるのかなとも思います。
    このタイマーインターバルの変動なのですが、今までは、タイマーインターバルが500msecより遅くなるということだったのですが、描画処理・データ受信処理を除いて、タイマーのみで
    動作させた場合、タイマーインターバルが早くなることがわかりました。
    質問にはあえて書かなかったのですが、20msecタイマーの25回で500msecとしているのですが、そのタイマーのインターバルが設定画面呼び出し時ですと、20msec毎ではなく
    0
    0
    20
    20
    .
    .
    のように、初めにインターバルが0で呼び出しているようです。私の憶測なのですが、これはインターバルが変化したというよりも同じタイマー処理を何度も呼び出そうとしているのでは
    ないかなという気がします。たとえば、20msecで1~25回タイマー処理をするとして、1回目のタイマー処理を設定画面を呼び出したときの影響によって何度も呼び出そうとしている
    のではということです。

    また、波形描画、データ受信処理をしながらタイマー処理時に、設定画面を呼び出したときのタイマーインターバルの遅延についてなのですが、たとえばその遅延発生時のタイマーインターバルは
    500msecではなく580msecだとすると、次のタイマー処理時には500msecではなく420msecのように今度はインターバルが早くなります。
    これはtimeSetEventによるコールバック関数の処理が設定画面の呼び出しの影響によって、溜まってしまうために、初めは処理の遅延がおこり、その次に遅延分を取り戻すために、一気に
    溜まっている処理をしている為だと思われます。
    そのため、私が今回かかえている問題はいかにしてメインダイアログからの他のダイアログの呼び出しをすることで、timeSetEventから呼び出されるCALLBACK関数への影響を少なく
    するかということだと思うんです。CALLBACK関数にてデータがプールさせないような方法はないものでしょうか?

    >また、TimeProc の中から SetEvent を呼び出すのが正しい ( というよりは WinCE での TimeProc 内での禁止事項として明記されていない ) 場合ですが、データ受信スレッドは >WaitForSingleObject で停止しているのでしょうか?

    >デー タの受信に時間が掛かり、イベントはシグナル状態になっているのに、スレッドが処理中で、処理終了後、WaitForSingleObject 呼び出しと同時に処理が始まる、という事は無いでしょうか?

    >イベントをシグナルにする直前と、データ受信スレッドの処理完了時刻を timeGetTime で取得し、データ受信が 0.5 秒以内に確実に終わっているかの検証も必要かと思います。
    ミッヒー様のご質問の件を検証してみましたところ、受信スレッドはWaitForSingleObjecで停止しているようです。
    また、データ受信処理については、時間がかかっても150msec程度(通常時は50msecほど)ですので、ミッヒー様の危惧していることは起こっていないようです。
    長文になりまして、申し訳ございません。

    2010年3月2日 1:27
  • え~と、なんか変な話になってますね(vv;)。
    一般にピリオディックでイベントタイプのMMタイマーは以下の様に
    起動しますが、そうなってないのでしょうか。
    // タイマー起動
    void MMTIMER_Satrt()
    {
    CEvent * Event = new CEvent();
    id = ::timeSetEvent( 20,  0,
            (LPTIMECALLBACK)Event->m_hObject,
            NULL, TIME_PERIODIC | TIME_CALLBACK_EVENT_PULSE);
    pTh_Timer = AfxBeginThread(TimeThread, this);
    }
    // タイマースレッド
    TimeThread(LPVOID pParam)
    {
     while(1){
      DWORD dwRet = ::WaitForSingleObject(
                    Event->m_hObject, INFINITE);
        :::
     }
    }
    擬似的コードです、意味が通じることを祈ります・・・(vv;)。
    2010年3月2日 2:14
  • すいません、仲澤@失業者 さんにも混乱を与える書き込みだったようで・・・。
    鏑木肆星 さんが作成中のアプリケーションは 20 ms 毎の画面更新と、500 ms 毎の複数データ受信処理を平行して行っているはずです。

    今回の質問に描画部分は含まれていませんが、私が前のスレッドから続く描画部分の話も一緒にした為に誤解を招いてしまったようです・・・。すいません。

    20 ms の画面更新は多少遅延が見られてもいいと思うのですが、500 ms のデータ受信処理は 500 ms 毎にデータが上書きされて無くなってしまう為、それまでにデータを読み込まなければならず、実際の時間が前後するであろうタイマー内での読み込みよりは、20 ms 毎のタイマーで 500 ms 経過しているかを判断し、そこからデータ読み込みスレッドをアクティブにすればいいと思っていたわけです。

    ただ、仲澤@失業者 さんのコードからいくと、20 ms にスレッドがアクティブになるのですから、そのスレッドからデータ受信スレッドをアクティブにする、という方法は使えるかと。ただ、データ受信用のスレッドを新しく作成する必要がありますね・・・。
    2010年3月2日 6:06
  • 仲澤@失業者様、ご回答ありがとうございます。

    理想的なタイマーの動きとしては、仲澤様のおっしゃられているコードであっていると思います。
    しかし、この20msecというタイマーインターバルが他ダイアログ呼び出しの影響によって、
    常に一定にはならないということが現状で問題となっております。
    もしかすればCEの仕様上、難しいのではないかと半ばあきらめてきています。。
    2010年3月2日 7:51
  • ミッヒー様、ご回答ありがとうございます。

    仲澤@失業者様が提示して頂いたように、受信スレッドを初めに作成し、timeSetEventにてイベントハンドルをタイマーインターバル毎にシグナル状態
    にし、受信スレッドをアクティブにする方法も以前に試してみたのですが、やはり、他ダイアログを呼ぶことによる影響としては変わりませんでした。
    ですが、どの方法も他ダイアログを呼ばない、そのままの状態であれば、波形の描画・データの受信には問題がないため、考え方や方法は間違って
    はいないと思います。
    やはり他ダイアログを呼び出す影響は、CEの仕様のためもあるのかなと思っております。。。むずかしいもんですね。。
    2010年3月2日 7:54
  • 親スレッドが止まると、その子スレッドも止まるように、スレッドは常にその親スレッドの
    影響下にあります。
    今回の場合「メインDLG」を操作するとスレッドに影響がでるということですので
    そのタイマースレッドを作成する位置をCWinApp派生クラスXXXXAppクラスの
    InitInstance()内の「メインDLG」を作成(DoModal())する前にしてみたらど゛うでしょう。
    もう試していて結局だめなら、打つ手がなくなってきますねぇ(vv;)。

    2010年3月2日 10:09
  • 親スレッドが止まると、その子スレッドも止まるように、スレッドは常にその親スレッドの
    影響下にあります。
    今回の場合「メインDLG」を操作するとスレッドに影響がでるということですので
    そのタイマースレッドを作成する位置をCWinApp派生クラスXXXXAppクラスの
    InitInstance()内の「メインDLG」を作成(DoModal())する前にしてみたらど゛うでしょう。
    もう試していて結局だめなら、打つ手がなくなってきますねぇ(vv;)。

    2010年3月2日 10:11
  • 仲澤@失業者 さんの言われるとおりですね・・・。後出来るとするなら、他プロセスを起動し、ミューテックスを用いたプロセス間でタイマー処理をするぐらいでしょうか・・・。

    ところで、完全に考えてなかったのですが、0.5 秒毎に受信するデータが上書きされてなくなってしまうという状態でしたよね?

    最初のデータ受信のタイミングが、データが更新された直後だという保証はあるのでしょうか?

    つまり、受信したデータが 0.45 秒ほど経過した時点でのデータで、データ受信中に次のデータに上書きされ、先頭のデータと途中からのデータが違う順序で取得されてしまう、という心配は無いでしょうか?
    アプリケーション起動後のデータ同期処理があるのか、データ取得を開始するまで、入力データの 0.5 秒毎の更新はないのでしょうか?

    それともう一点ですが、タイマーは 20 ms の画面更新用のもの一つで、そのタイマー処理の中で経過時間を計測し、500 ms 経過していれば、データ受信を行う、という形式でしょうか?

    今更、という質問ですいません・・・。

    # 仲澤@失業者 さんの言われる、親スレッドが止まると、その子スレッドも止まる、というのは親スレッドがビジー処理に入り、あたかも止まったように見える状態になった場合、子スレッドが処理を終えて、再び停止しても、次の処理命令が届かない為に動きようがない、という解釈であっていますか?
    2010年3月3日 7:13
  • # 仲澤@失業者 さんの言われる、親スレッドが止まると、その子スレッドも止まる、というのは親スレッドがビジー処理に入り、あたかも止まったように見える状態になった場合、子スレッドが処理を終えて、再び停止しても、次の処理命令が届かない為に動きようがない、という解釈であっていますか?
    もちろん、あるプロセス内のスレッドは優先度が同じならスケジューラからは
    理想的には平等に扱われます。何の共有リソースも使っていなければの話ですが。
    想定している場面はSleep()で止まっているスレッドはメッセージキューも止まっている
    ので、話しかけても無駄ということです。もう一つはCEの場合は動作が異なるかも
    しれませんが、ある程度、高負荷なジョブ中は「理想的」な状態にはないだろうと、
    想像しています。鏑木肆星さんのケースで、コードが適切であると仮定すると、
    残されているのは
     1.UIをスレッドにして、実行プライオリティを下げる。
     2.20[ms]の画面更新頻度を落とす。
    くらいしか思いつかないほど悲観的になります。
    2010年3月3日 9:25
  • もちろん同一プロセス内のスレッドはスケジューラから平等にあつかわれますが、
    共有のリソースが全くない、また十分なCPUの余裕がある等の理想的条件の場合
    のみです。想像しているのはSleep()しているスレッドに話しかけても無駄ということと、
    今回のケースの様に高負荷が想像される場合は、平等に扱われてるのだろうか
    という疑いです。残されているのは
     1.UIを子スレッド化して実行プライオリティを下げる
     2.20[ms]の描画更新頻度を落とす
    くらいしかないのかなということです。
    2010年3月3日 9:34