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

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

MFC ダイアログでファイル読込、集計、通信待ちのような重い処理を UI スレッドで実行すると、ウィンドウが「応答なし」に見えます。この記事では、AfxBeginThread でワーカースレッドを起動し、進捗・完了・キャンセルを PostMessage で UI スレッドへ戻す実装手順を確認します。帳票/CSV取り込みのようなバッチ処理を想定し、スレッドから CWnd を直接触らない理由も扱います。

目次

まず確認するもの

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* を渡して、ワーカースレッドから直接メンバ関数を呼ぶ形にしないのが重要です。UI に通知したい先は 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;

    CWinThread* pThread = AfxBeginThread(&CMyDialog::WorkerProc, param);
    if (pThread == nullptr)
    {
        delete param;
        ShowStartError();
        return;
    }

    m_workerThreadId = pThread->m_nThreadID;
    SetRunningUiState();
}

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

ワーカースレッドの処理本体では、時間のかかる処理だけを行います。進捗や完了状態は、ユーザー定義メッセージとして UI スレッドへ投げます。通知前に 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)
        {
            if (::IsWindow(param->notifyHwnd))
            {
                ::PostMessage(param->notifyHwnd, WMU_WORKER_COMPLETE, TRUE, index);
            }
            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 がまだ固まる重い処理が UI スレッド側に残っているイベントハンドラ内のループ、待機、I/O重い処理を WorkerProc 側へ移す
進捗バーが更新されないメッセージマップに受信側がないON_MESSAGE とハンドラ名ユーザー定義メッセージを正しく受ける
キャンセルが遅いキャンセル確認の間隔が長いループ内の確認位置安全な区切りごとにフラグを見る
完了後にボタンが戻らない完了通知が UI 側へ届いていないPostMessage の宛先 HWND有効な HWND を渡す
たまに不安定になるワーカースレッドから CWnd を直接操作しているWorkerProc 内の UI 操作PostMessage 経由に戻す
終了時に不安が残る処理中の閉じる操作を許しているOnCloseOnCancel処理中はキャンセル完了を待つ

AfxBeginThreadと他の手段の使い分け

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

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

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

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

まとめ

  • AfxBeginThread は、MFC でワーカースレッドを作る基本手段です。
  • UI フリーズを防ぐには、時間のかかる処理を UI スレッドから切り離します。
  • ワーカースレッドへ渡す情報は、HWND、キャンセルフラグ、処理件数など最小限にします。
  • 進捗・完了・キャンセルの画面更新は、PostMessage を受けた UI スレッド側で行います。
  • ワーカースレッドから CWnd やコントロールを直接操作しないようにします。
  • キャンセルは強制終了ではなく、フラグで要求して安全な区切りで抜けます。
  • 閉じる処理では、スレッドが残ったまま画面を破棄しないように確認します。
目次