none
SetTimerでリターンコードが0で戻る (Return Code = 0)

    質問

  • お世話になります。

    環境: Windows 10 Pro 64Bit + VS 2010 Pro

    アプリケーションの形態: MFCのダイアロブベースのアプリ

    問題:

    OnInitDialog内で複数タイマー識別子と共にSetTimerを掛けON_TIMERでタイマー識別子毎にswitch/caseである処理を行っています。 タイマープロシージャTIMERPROCは使用していません。

    このswitch/case内であるタイマー識別子を一旦KillTimerしタイムアウト値を変え再度SetTimer(5分から30分の範囲で変化)を掛けています。 このアプリは暫くは(4時間から24時間位)正常に稼働するのですが、いきなりSetTimerのリターンコードが0で戻りタイマーが掛からなくなります。

    質問:

    Q1. ON_TIMER内でその時に発生したタイマー識別子に対しKillTimer/SetTimerを掛けても問題ないのでしょうか。

    Q2. 今回は使用していませんが、TIMEPROCを使用した場合にそのPROC内で自分自身のタイマー識別子をKillTimer/SetTimer掛けることは問題ないのでしうか。

    アドバイス頂きたく宜しくお願い致します。 

    2018年2月12日 14:00

回答

  • Q1. ON_TIMER内でその時に発生したタイマー識別子に対しKillTimer/SetTimerを掛けても問題ないのでしょうか。

    ドキュメントでは特に言及されていませんし、KillTimerなんかはON_TIMER中に中止したくなるのはよくあるパターンですし、問題ないかと思います。

    Q2. 今回は使用していませんが、TIMEPROCを使用した場合にそのPROC内で自分自身のタイマー識別子をKillTimer/SetTimer掛けることは問題ないのでしうか。

    同じく問題ないかと。ただし、MFCのCWnd::SetTimerドキュメントでのみ言及されている注意点として「コールバックのタイマーの場合、(nIDEventの)値はすべてのプロセスのすべてのタイマーで一意である必要があります。」とのことです。自プロセス内で一意とすることは簡単ですが、全プロセスで一意とするのは事実上不可能です。作成されるTimerIDは引数で指定した値とは一致しないであろうことを前提に設計する必要があります。
    Windows APIのSetTimerドキュメントでは言及されていませんが、問題の性質上、SetTimerを直接呼び出した場合にも適用される制約と思われます。

    このアプリは暫くは(4時間から24時間位)正常に稼働するのですが、いきなりSetTimerのリターンコードが0で戻りタイマーが掛からなくなります。

    Windows APIのSetTimerドキュメントにあるように、0が返るのはエラー時であり、GetLastErrorでエラー理由を知ることができます。
    勝手な想像ではありますが、使い終わったタイマーを正しく削除できていないのかと。これもWindows APIの方のドキュメントにありますが、第1引数HWNDにNULLを与えた場合は戻り値が新しいTimerIDなのに対し、HWNDにNULLを与えなかった場合は第2引数で指定した値がTimerIDであり戻り値は「nonzero integer」としか書かれていません。この説明を読む限り、MFCのCWnd::SetTimerドキュメントにある「アプリケーションがタイマーを終了するには、必ず KillTimer メンバー関数に戻り値を渡す必要があります。」は嘘っぽく感じられますね。

    • 回答としてマーク HIRA_TKA 2018年2月13日 14:09
    2018年2月12日 14:56

すべての返信

  • Q1. ON_TIMER内でその時に発生したタイマー識別子に対しKillTimer/SetTimerを掛けても問題ないのでしょうか。

    ドキュメントでは特に言及されていませんし、KillTimerなんかはON_TIMER中に中止したくなるのはよくあるパターンですし、問題ないかと思います。

    Q2. 今回は使用していませんが、TIMEPROCを使用した場合にそのPROC内で自分自身のタイマー識別子をKillTimer/SetTimer掛けることは問題ないのでしうか。

    同じく問題ないかと。ただし、MFCのCWnd::SetTimerドキュメントでのみ言及されている注意点として「コールバックのタイマーの場合、(nIDEventの)値はすべてのプロセスのすべてのタイマーで一意である必要があります。」とのことです。自プロセス内で一意とすることは簡単ですが、全プロセスで一意とするのは事実上不可能です。作成されるTimerIDは引数で指定した値とは一致しないであろうことを前提に設計する必要があります。
    Windows APIのSetTimerドキュメントでは言及されていませんが、問題の性質上、SetTimerを直接呼び出した場合にも適用される制約と思われます。

    このアプリは暫くは(4時間から24時間位)正常に稼働するのですが、いきなりSetTimerのリターンコードが0で戻りタイマーが掛からなくなります。

    Windows APIのSetTimerドキュメントにあるように、0が返るのはエラー時であり、GetLastErrorでエラー理由を知ることができます。
    勝手な想像ではありますが、使い終わったタイマーを正しく削除できていないのかと。これもWindows APIの方のドキュメントにありますが、第1引数HWNDにNULLを与えた場合は戻り値が新しいTimerIDなのに対し、HWNDにNULLを与えなかった場合は第2引数で指定した値がTimerIDであり戻り値は「nonzero integer」としか書かれていません。この説明を読む限り、MFCのCWnd::SetTimerドキュメントにある「アプリケーションがタイマーを終了するには、必ず KillTimer メンバー関数に戻り値を渡す必要があります。」は嘘っぽく感じられますね。

    • 回答としてマーク HIRA_TKA 2018年2月13日 14:09
    2018年2月12日 14:56
  • 佐祐理 さま

    早々のアドバイスありがとうございました。

    SetTimerのリターンコード0後にGetLastErrorでエラーの理由を確認したところ「1158 現在のプロセスがWindow Manager オブジェクトのハンドルのシステム許容範囲をすべて使用しました。」でした。 佐祐理 さまの言われるようにプログラム内の別クラスでKillTimerが失敗し、その後のSetTimerでタイマー識別子を指定しているのですが、毎回違ったリターンコード(タイマー識別子)が戻ってきていました。 これがオブジェクトのハンドルを無限に消費したようです。 

    このKillTimer/SetTimer部分をSendMessageに変更し主クラス内で一括してKillTimer/SetTimer処理とリターンコードをチェックするように変更しました。 

    目下、確認のためプログラムを継続実行中です。 また、念のためプロセス毎のハンドス数の増減を他の方法で確認できないかも調査中です。

    お忙しい中的確なアドバイスを頂きましてありがとうございました。  

    2018年2月13日 14:06
  • 後学のためSetTimerに失敗するパターンでのコードがどのようになっているかを教えてください。

    1. CWnd::SetTimerを使用している or ::SetTimerを使用し第1引数HWNDはNULL以外を指定しているかどうか。
    2. INT_PTR nIDEventに毎回異なる値を指定しているかどうか。
    3. callbackを指定しているかどうか。
    4. CWnd::KillTimerを使用している or ::KillTimerを使用し第1引数HWNDは1.と同じ値かどうか。
    5. KillTimerに渡す値は2.で指定したnIDEventか or SetTimerの戻り値か。

    その上で、5. の内容を反転(nIDEventを渡していたのであればSetTimerの戻り値を渡すように変える、またはその逆)した場合に改善されるかどうか。

    # 4.がきちんと押さえられていて、5.またはその逆によりタイマーは正しく破棄できるようになると思ってはいますが、念のため、と。

    2018年2月13日 14:32
  • アドバイスを頂き色々調べてみましたが、結果全く間違えた使い方をしていました。

    タイマー使用の概要:

    今回問題のSetTimer/KillTimerはDialog画面の最下位置(pane)に色々な処理毎(別クラスや別オブジェクト)のメッセージを一定時間表示するためのタイマー(表示時間を10秒~20秒)です。 よって、メッセージ表示後SetTimerを掛け一定時間経過後KillTimerを掛けメッセージをクリアーしています。 これはプログラムの処理が次の段階に進んでいるにも関わらず、古いメッセージを表示しっぱなしにしておく事を避けるためです。


    タイマーの使用箇所:

    このSetTimer/KillTimerは主Dialogソースプログラムとメッセージを表示するソースプログラム(CStatusBar::SetPaneText使用)で使用しています。 メッセージを表示するプログラムはSTATIC (今回の問題が何処にあるか不明だったため色々トライし現在はINLINE状態)関数としています。

    ------

    1. CWnd::SetTimerを使用している or ::SetTimerを使用し第1引数HWNDはNULL以外を指定しているかどうか。

    CDialogのOnInitDialog内のコーディング:

    #define STI_STATUS_BARMSG 0x00004000


    SetTimer (STI_STATUS_BARMSG, nnn, NULL);

    KillTimer (STI_STATUS_BARMSG);

    CDialogの中では第1引数はタイマー識別子が要求され、HWNDを指定する余地がありませんでした。


    STATICプログラム内のコーディング:

    SetTimer(0, STI_STATUS_BARMSG, nnn, NULL)

    KillTimer(0, STI_STATUS_BARMSG);

    -----

    2. INT_PTR nIDEventに毎回異なる値を指定しているかどうか。

    毎回同じ値(STI_STATUS_BARMSG)を使用しています。

    -----
    3. callbackを指定しているかどうか。

    指定していません。

    -----
    4. CWnd::KillTimerを使用している or ::KillTimerを使用し第1引数HWNDは1.と同じ値かどうか。

    #1の通りです。

    -----
    5. KillTimerに渡す値は2.で指定したnIDEventか or SetTimerの戻り値か。

    #1の通りです。

    -----
    失敗するパターンでのコードがどのようになっているかを教えてください。


    CDialogプログラムの方:

    SetTimer: 毎回0x00004000 - 問題なし

    KillTimer: 毎回 1 - 問題なし

    STATICプログラムの方:


    SetTimer : 21732, 21652 ,,, - 問題あり

    KillTimer : 0 - 問題あり

    -----
    結果STATICプログラム内でのタイマーが正しく働かなく、CDiglogプログラム内のタイマーが一貫して使われていたと言う事になります。 勉強になりました。

    p.s.

    このサイトでの質問が初めてで迅速なご対応で大変助かりました。 

    因みに、前のアドバイスで私の質問部分に対してシェードが掛かっていますが、どのような操作でこのシェードを掛けるのでしょうか。 

    2018年2月14日 3:48
  • SetTimer (STI_STATUS_BARMSG, nnn, NULL);
    SetTimer(0, STI_STATUS_BARMSG, nnn, NULL)

    ちなみにSetTimerはnIDEventが一致するとhWndを指定しなくても既存のタイマーを置き換える機能があります。しかしKillTimerしてしまってからstaticなSetTimer(0, STI_STATUS_BARMSG, nnn, NULL)を呼び出してしまうと新規タイマーが作成され、その値はきっとSTI_STATUS_BARMSGではないため、KillTimerには常に失敗しタイマーリーク、という流れでしょうか。

    今回問題のSetTimer/KillTimerはDialog画面の最下位置(pane)に色々な処理毎(別クラスや別オブジェクト)のメッセージを一定時間表示するためのタイマー(表示時間を10秒~20秒)です。 よって、メッセージ表示後SetTimerを掛け一定時間経過後KillTimerを掛けメッセージをクリアーしています。 これはプログラムの処理が次の段階に進んでいるにも関わらず、古いメッセージを表示しっぱなしにしておく事を避けるためです。

    この用途でしたら状況に応じてタイマーをON / OFFせずとも常に1秒間隔でタイマーを作動させておき、OnTimer処理内で時刻を見ながら表示/非表示を制御した方が簡単なのでは。

    因みに、前のアドバイスで私の質問部分に対してシェードが掛かっていますが、どのような操作でこのシェードを掛けるのでしょうか。

    引用ボタンを押すと<blockquote>タグが挿入されます。その後はHTMLを直接編集したりもします(本フォーラムの投稿編集画面はとても貧弱なので…)。

    2018年2月14日 4:45
  • > また、念のためプロセス毎のハンドス数の増減を他の方法で確認できないかも調査中です。

    既に解決済みなので、どーでもいいことかもしれませんが。。。。
    Timer Object のリークだったら、ライブ デバッグか完全メモリ ダンプで、"!timer" コマンド使えば確認できるかも。
    (カーネル モード デバッグ限定のコマンドだけど、Hyper-V や VMWare 等の仮想環境があれば、簡単にデバッガをアタッチできます。)
    -------------------------------------------
    !timer
    https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/-timer
    -------------------------------------------
    このコマンド、Timer Object や Thread のアドレスを表示してくれるから、結構簡単にリークを見つけられるかも。。。。。と思って試したら、フツーに動いている状態で、既にわんさかリークしているような。。。
    2018年2月14日 9:20
  • 佐祐理 さま、

    アドバイスありがとうございます。

    この用途でしたら状況に応じてタイマーをON / OFFせずとも常に1秒間隔でタイマーを作動させておき、OnTimer処理内で時刻を見ながら表示/非表示を制御した方が簡単なのでは。

    他のタイマーも含め検討したいとおもいます。

    引用ボタンを押すと<blockquote>タグが挿入されます。その後はHTMLを直接編集したりもします(本フォーラムの投稿編集画面はとても貧弱なので…)。

    なるほど、HTMLは初心者ですが編集できたようです。

    ありがとうございました。


    2018年2月14日 13:57
  • 引き続きアドバイスありがとうございます。

    !timerを含め「Windows Debugging Tools」を勉強してみます。

    Hyper-VもVMWareの環境がないのでカーネルモードのデバックは残念ですができません。

    落ち着いたら次の方法を試してみようと思っていました。

    https://technet.microsoft.com/ja-jp/windows/mark_07.aspx?f=255&MSPPError=-2147217396

    ありがとうございます。

    2018年2月14日 14:06