ワーカースレッドの停止用に作ったイベントを、スレッドがまだ使っている間にダイアログ側でCloseHandleしてしまう。その後でSetEventを呼ぶと戻り値は0、GetLastError()は6、つまりERROR_INVALID_HANDLEです。この記事では、閉じたハンドルを使い回したこの例を土台に、ハンドルがどこで無効になったのかを切り分けます。
GetLastError 6で最初に確認するもの
| 見る場所 | 見る値 | 何を疑うか |
|---|---|---|
| 失敗したAPI | 戻り値 | まず戻り値で失敗を確定させ、それからGetLastError()を読む |
| エラーコード | 6(0x6) | ERROR_INVALID_HANDLE。ハンドル値そのものが無効 |
| 生成箇所 | CreateEventなどの戻り値 | 生成に失敗した値や、未代入のまま渡していないか |
| 破棄箇所 | CloseHandleの位置 | 閉じたあとの値が、別の変数やスレッドに残っていないか |
| 所有者 | 生成・利用・破棄する側 | 同じハンドルを二か所以上が閉じていないか |
Microsoftのシステムエラーコード一覧でも、6(0x6)はERROR_INVALID_HANDLEです。ただし、この番号が教えてくれるのは「渡したハンドルが無効だった」ことだけで、どこで無効になったかは別問題です。生成、受け渡し、破棄のどこで壊れたかを追うことになります。
CloseHandle後の再利用でエラー6になる
閉じたあとのハンドル値は、見た目は生きていたときと同じ数値のままです。だからSetEventにそのまま渡せてしまい、実行時に初めて失敗します。SetEventは失敗すると0を返すので、その値を見た直後にエラーコードを取ります。次のコードが、検証環境で6を再現させた最小の形です。
HANDLE hEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (hEvent == nullptr)
{
const DWORD error = ::GetLastError();
return;
}
::CloseHandle(hEvent);
if (!::SetEvent(hEvent))
{
const DWORD error = ::GetLastError();
// 検証環境では error == ERROR_INVALID_HANDLE (6)
}
// 実コードではCloseHandle直後に無効化する
hEvent = nullptr;

CloseHandleの仕様によれば、この呼び出しはハンドルを無効にします。つまり悪いのはCloseHandleではなく、そのあとも古い値を持ち続けている側です。なお、無効な値をもう一度CloseHandleへ渡すとデバッガ実行中に例外になるため、二重クローズは再現手順に入れていません。
無効になった場所を3段階で追う
- 生成。ハンドルを返すAPIの戻り値を見て、成功したときだけメンバー変数へ入れます。
- 受け渡し。代入や引数渡し、ワーカースレッドへ渡す経路をたどり、まだ使っている最中に別の場所が閉じていないかを見ます。
- 破棄。
CloseHandleのすぐ後で変数をnullptrへ戻し、閉じる責任を一か所に寄せます。
MFCのダイアログやドキュメントクラスが生のHANDLEをメンバーで持つときは、生成したクラスが破棄まで面倒を見る、と決めておくのが安全です。別スレッドがそのハンドルを待っているなら、スレッドを止めて待機を終えてから閉じます。順番を逆にすると、待っている側が無効なハンドルをつかんだままになります。
void CWorkerOwner::CloseStopEvent()
{
if (m_hStopEvent != nullptr)
{
::CloseHandle(m_hStopEvent);
m_hStopEvent = nullptr;
}
}
nullptrへの代入は、残っている別名のハンドルまで無効化するものではありません。同じ値を複数のメンバーやスレッドへコピーしている場合は、コピー元を含めて所有関係を見直します。
CloseHandleはすべてのハンドル型に使う関数ではありません。たとえばソケットはclosesocket、レジストリキーはRegCloseKeyで閉じます。生成APIのドキュメントで対応する破棄関数も確認してください。
GetLastErrorは失敗直後に保存する
SetEventは、成功時に0以外、失敗時に0を返し、詳細はGetLastErrorで取得します。エラー表示やログ出力を先に呼ばず、失敗を判定した行の直後で保存してください。
if (!::SetEvent(m_hStopEvent))
{
const DWORD error = ::GetLastError();
CString message;
message.Format(_T("SetEvent failed: %lu"), error);
m_status.SetWindowText(message);
}
GetLastErrorが返すのは、そのスレッドで最後に記録された値だけです。あいだに別のAPI呼び出しやログ出力をはさむと、6ではなく無関係な値に上書きされていることがあります。読むのは失敗を判定した行の直後、というのが結局いちばん確実です。タイミングの話は「GetLastError()は失敗直後に読む」で詳しく扱っています。
まとめ
- GetLastError 6は
ERROR_INVALID_HANDLEです。 - コード6だけでは原因箇所は決まりません。生成、受け渡し、破棄の順にたどります。
CloseHandle後は変数をnullptrへ戻し、所有者を一つにします。- 失敗したAPIの戻り値を見てから、
GetLastError()を保存します。
