【MFC】AfxBeginThread: ワーカースレッドの作り方

【MFC】AfxBeginThread: ワーカースレッドの作り方

MFCダイアログでファイル読込や集計、通信待ちのような重い処理をUIスレッドで実行すると、ウィンドウが「応答なし」になります。AfxBeginThreadでワーカースレッドを起動し、進捗・完了・キャンセルをPostMessageでUIスレッドへ戻すと、これを避けられます。帳票やCSVの取り込みのようなバッチ処理を想定したコードです。

目次

まず確認するもの

AfxBeginThreadを使う前に、別スレッドへ移す処理と、UIへ戻す情報を分けておきます。分けておけば、CWnd* をスレッドと共有せずに済みます。

やりたいこと使う手段UIへ戻す情報注意点
重い処理を裏で進めるAfxBeginThread完了件数、現在の処理、完了結果処理本体はUIスレッドから切り離す
進捗バーや処理キューを更新する::PostMessage現在値、処理中の項目、最大件数ワーカースレッドからコントロールを直接操作しない
キャンセルを受け付ける共有フラグ停止済みか、何件まで終わったかTerminateThreadで強制終了しない
完了後にボタン状態を戻すユーザー定義メッセージ成功、キャンセル、エラーUI変更はメッセージハンドラ側で行う

検証環境と前提

コード例はMFCのダイアログアプリが前提です。ワーカースレッドが重い処理を受け持ち、画面の更新はUIスレッドだけで行います。

項目内容
OSWindows 11
IDEVisual Studio 2026
プロジェクトMFCダイアログベースアプリ
文字セットUnicode
MFCの使用共有DLLでMFCを使用
構成Debug / Release、Win32 / x64

AfxBeginThreadで作るワーカースレッドの全体像

ワーカースレッドは、UIを持たないバックグラウンド処理です。MFCではAfxBeginThreadにスレッド関数とパラメータを渡すだけで、CWinThreadオブジェクトの作成とスレッド開始を任せられます。

// UI スレッドへ戻すメッセージを決める
constexpr UINT WMU_WORKER_PROGRESS = WM_APP + 1811;
constexpr UINT WMU_WORKER_COMPLETE = WM_APP + 1812;

// AfxBeginThread に渡す関数は、AFX_THREADPROC 相当の形にする
static UINT AFX_CDECL WorkerProc(LPVOID pParam);

UINT AFX_CDECL CMyDialog::WorkerProc(LPVOID pParam)
{
    // 重い処理をここで実行する
    return 0;
}

// UI 側からスレッドを開始する
CWinThread* pThread = AfxBeginThread(&CMyDialog::WorkerProc, pParam);
if (pThread == nullptr)
{
    // 起動失敗時は UI スレッド側で後始末する
}
AfxBeginThreadでワーカースレッドを起動する前のバッチ処理ダッシュボード初期状態

スレッドへ渡すパラメータは最小限にする

AfxBeginThreadの第2引数で、ワーカースレッドへ渡せる値は1つだけです。HWND、キャンセルフラグ、処理件数などは、構造体にまとめて渡します。

このとき、CDialog* やCWnd* を渡して、ワーカースレッドからメンバ関数を直接呼ぶ形にはしません。通知したい先はHWNDで持っておき、画面の更新は、メッセージを受けたUIスレッドが行います。

struct WorkerParam
{
    HWND notifyHwnd;
    volatile LONG* cancelRequested;
    int totalItems;
};

void CMyDialog::StartWorker()
{
    ::InterlockedExchange(&m_cancelRequested, 0);

    auto* param = new WorkerParam;
    param->notifyHwnd = GetSafeHwnd();
    param->cancelRequested = &m_cancelRequested;
    param->totalItems = kTaskCount;

    // m_nThreadID を読むまで止めておく(走らせると終了時に CWinThread が自分を削除する)
    CWinThread* pThread = AfxBeginThread(&CMyDialog::WorkerProc, param,
        THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED);
    if (pThread == nullptr)
    {
        delete param;
        ShowStartError();
        return;
    }

    m_workerThreadId = pThread->m_nThreadID;
    pThread->ResumeThread();  // ここから先は pThread に触らない
    SetRunningUiState();
}

WorkerProcでは重い処理だけを実行する

ワーカースレッドの処理本体には、時間のかかる処理だけを書きます。進捗と完了は、ユーザー定義メッセージでUIスレッドへ投げます。通知の前に、IsWindowでHWNDが有効か見ておきます。

#include <memory>

UINT AFX_CDECL CMyDialog::WorkerProc(LPVOID pParam)
{
    if (pParam == nullptr)
    {
        return 1;
    }

    std::unique_ptr<WorkerParam> param(
        static_cast<WorkerParam*>(pParam));

    for (int index = 0; index < param->totalItems; ++index)
    {
        if (::InterlockedCompareExchange(param->cancelRequested, 0, 0) != 0)
        {
            if (::IsWindow(param->notifyHwnd))
            {
                ::PostMessage(param->notifyHwnd, WMU_WORKER_COMPLETE, TRUE, index);
            }
            return 0;
        }

        DoHeavyWorkOneItem(index);

        if (::InterlockedCompareExchange(param->cancelRequested, 0, 0) != 0)
        {
            // この項目は処理し終えているので、完了件数は index + 1
            if (::IsWindow(param->notifyHwnd))
            {
                ::PostMessage(param->notifyHwnd, WMU_WORKER_PROGRESS, index + 1, index);
                ::PostMessage(param->notifyHwnd, WMU_WORKER_COMPLETE, TRUE, index + 1);
            }
            return 0;
        }

        if (::IsWindow(param->notifyHwnd))
        {
            ::PostMessage(param->notifyHwnd, WMU_WORKER_PROGRESS, index + 1, index);
        }
    }

    if (::IsWindow(param->notifyHwnd))
    {
        ::PostMessage(param->notifyHwnd, WMU_WORKER_COMPLETE, FALSE, param->totalItems);
    }
    return 0;
}
ワーカースレッド実行中に処理キューとUI応答確認とスレッドIDが更新されている状態

UI更新はPostMessageの受信側で行う

MFCオブジェクトは、複数のスレッドから操作しても安全とは限りません。特にCWnd系のオブジェクトは、Windowsハンドルマップがスレッドごとに管理されるため、ワーカースレッドから直接UIを操作すると、更新されない、別のオブジェクトとして扱われる、タイミングによって動きが変わる、といったことが起きます。

BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
    ON_MESSAGE(WMU_WORKER_PROGRESS, &CMyDialog::OnWorkerProgress)
    ON_MESSAGE(WMU_WORKER_COMPLETE, &CMyDialog::OnWorkerComplete)
END_MESSAGE_MAP()

LRESULT CMyDialog::OnWorkerProgress(WPARAM wParam, LPARAM lParam)
{
    const int completedItems = static_cast<int>(wParam);
    const int currentIndex = static_cast<int>(lParam);

    m_progress.SetRange32(0, kTaskCount);
    m_progress.SetPos(completedItems);
    SetTaskState(currentIndex, _T("完了"));

    CString text;
    text.Format(_T("実行中... %d / %d 件完了"), completedItems, kTaskCount);
    m_status.SetWindowText(text);
    return 0;
}

キャンセルは強制終了ではなく要求として扱う

ワーカースレッドを止めたいときは、強制終了せずに、キャンセル要求を送ります。ファイル、ロック、メモリ確保、外部リソースを扱う処理を途中で強制的に止めると、後始末ができなくなるからです。

void CMyDialog::CancelWorker()
{
    if (!m_isWorkerRunning)
    {
        return;
    }

    ::InterlockedExchange(&m_cancelRequested, 1);
    m_status.SetWindowText(_T("キャンセル要求を送信しました..."));
}
ワーカースレッドへキャンセル要求を送信し未実行タスクが残っている状態

閉じる処理ではスレッドの生存を確認する

ダイアログが破棄されたあとにワーカースレッドがPostMessageすると、通知先のHWNDはもう無効になっているかもしれません。そこで、閉じる操作のときに処理中なら、キャンセル要求だけ出して閉じません。完了メッセージを受けたあとで、閉じられる状態へ戻します。

void CMyDialog::OnClose()
{
    if (m_isWorkerRunning)
    {
        CancelWorker();
        m_status.SetWindowText(_T("処理中です。キャンセル完了後に閉じてください。"));
        return;
    }

    CDialogEx::OnClose();
}

よくある失敗パターン

症状よくある原因確認する場所修正方針
UIがまだ固まる重い処理がUIスレッド側に残っているイベントハンドラ内のループ、待機、I/O重い処理をWorkerProc側へ移す
進捗バーが更新されないメッセージマップに受信側がないON_MESSAGEとハンドラ名ユーザー定義メッセージを正しく受ける
キャンセルが遅いキャンセル確認の間隔が長いループ内の確認位置安全な区切りごとにフラグを見る
完了後にボタンが戻らない完了通知がUI側へ届いていないPostMessageの宛先HWND有効なHWNDを渡す
たまに不安定になるワーカースレッドからCWndを直接操作しているWorkerProc内のUI操作PostMessage経由に戻す
終了時に落ちることがある処理中の閉じる操作を許しているOnClose、OnCancel処理中はキャンセル完了を待つ

AfxBeginThreadと他の手段の使い分け

手段向いている用途避けたい用途
AfxBeginThreadのワーカースレッド計算、ファイル処理、短〜中時間のバックグラウンド処理スレッド内でウィンドウを所有する処理
MFCのUIスレッド別スレッドでメッセージループやウィンドウを持つ処理単純な一括処理
std::threadMFC非依存の処理MFCオブジェクトを直接扱う処理
タイマー分割軽い処理を少しずつ進める画面処理CPU負荷や待機が大きい処理

実装後に見るべき画面状態

処理が完了するかだけでなく、処理中もUIが応答しているかを見ます。処理キューが進んでいても、画面操作やタイマー更新が止まっているなら、重い処理がUIスレッドに残っている可能性があります。UIスレッドIDとワーカースレッドIDが別の値で表示されているかも、あわせて見ます。

バッチ処理が完了し処理キューと結果サマリでUI応答を確認できる状態

まとめ

  • AfxBeginThreadで、時間のかかる処理をUIスレッドから切り離します。
  • ワーカースレッドへ渡すのは、HWND、キャンセルフラグ、処理件数など最小限にして、CWndやコントロールは直接触りません。
  • 進捗・完了・キャンセルの画面更新は、PostMessageを受けたUIスレッドで行います。
  • キャンセルは強制終了せず、フラグで要求して、区切りのよい所で抜けます。閉じる操作では、スレッドが残ったまま画面を破棄しないようにします。

進捗や完了の通知をSendMessageで送ると、ワーカースレッドはUIスレッドがそのメッセージを処理し終えるまで戻りません。そのあいだにUIスレッドがワーカーの終了を待っていたり、ワーカーが持っているロックを取りに行ったりすると、両方のスレッドが止まります。どの組み合わせで固まるかは「PostMessage vs SendMessageの決定的違い」で確かめられます。

目次