LoginSignup
8
8

More than 5 years have passed since last update.

出来るだけコピペでマルチスレッド(MFC,C++)

Last updated at Posted at 2018-03-19

検証環境

Visual Studio 2013 Professional
多分、Communityとかでも動作するはず

概要

  • メインスレッドとは別のワーカースレッドを作って裏で作業させる。
  • 出来るだけ簡単に。
  • 出来るだけ見やすく。
  • 出来るだけコピペで。

テストアプリケーションの動作

  • ボタンを押すとボタンの表示が1秒ごとに変化する。それだけ。

仕組み

ボタンクリックでワーカースレッドの開始。
ワーカースレッドからPostMessageを飛ばしてメインスレッドがフォームの描画を更新(ボタンの表示を更新)。
もう一度ボタンを押すとワーカースレッドを終了させる。

下準備

  1. プロジェクト ダイアログベースとかシングルドキュメントとか、とにかくフォームがあるMFCプロジェクト作成。 今回のサンプルのプロジェクト名はTestMultiThreadでMFCアプリケーションのダイアログ形式に設定。
  2. クラス作成 ワーカースレッドをメンバに持つクラスをCThreadClassクラスとして作成。 例えばソリューションエキスプローラーからプロジェクトを右クリック->追加->クラス(C)...でクラスを作成。

フォームの編集

ビュークラスのヘッダを下記のとおりに修正(簡単にするためにビューにインスタンスを設定してますが、他の場所に設置しても可)

CTestMultiThreadDlg.h
#pragma once
// ↓を追加
// スマートポインタ用
#include <memory>
// スレッドクラス
#include "ThreadClass.h"
// -- 省略
class CTestMultiThreadDlg : public CDialogEx
{ // CTestMultiThreadDlgの宣言の中
// -- 省略
// ここから追記
public:
    // ワーカーからの命令を受けてメインスレッドが実行する関数。
    LRESULT OnMessage(WPARAM w_param, LPARAM l_param);
private:
    // ワーカーのインスタンス
    std::unique_ptr<CThreadClass>m_thread;
//ここまで追記
};

ビュークラスの実装側

TestMultiThreadDlg.cpp
// どこか上方に記述
// メッセージ番号の追加(定義した値が他とかぶらないように注意)
#define WM_MYMSG (WM_APP + 1)

// -- 省略
// CTestMultiThreadDlgのメッセージマップ
BEGIN_MESSAGE_MAP(CTestMultiThreadDlg, CDialogEx)
    ON_WM_SYSCOMMAND()
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    // ↓メッセージマップにこれを追加
    ON_MESSAGE(WM_MYMSG, CTestMultiThreadDlg::OnMessage)
END_MESSAGE_MAP()
// -- 省略

BOOL CTestMultiThreadDlg::OnInitDialog()
{
  // OnInitDialogの下方に追記
  // -- 省略
    // TODO: 初期化をここに追加します。
    // ここから追記
    m_thread.reset(new CThreadClass());
    m_thread->SetMessageNumber(WM_MYMSG);
    // ここまで追記

    return TRUE;  // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}
// -- 省略
// どこでも良いけど、下の方に追記
LRESULT CTestMultiThreadDlg::OnMessage(WPARAM w_param, LPARAM l_param)
{
    CButton *button = static_cast<CButton*>(GetDlgItem(IDC_BUTTON_TEST));
    button->SetWindowText(m_thread->GetValue());
    return 0;
}

ボタンを貼り付けて、そのプロパティでIDを「IDC_BUTTON_TEST」にしてから貼り付けたボタンをダブルクリックしてボタンの動作を追加。

TestMultiThreadDlg.cpp
void CTestMultiThreadDlg::OnBnClickedButtonTest()
{
    // TODO: ここにコントロール通知ハンドラー コードを追加します。
    // ここから追記
    if (m_thread->IsThread())
    {
        m_thread->StopThread();
    } else
    {
        m_thread->StartThread(this);
    }
    // ここまで追記
}

作っておいたThreadClassの宣言を下記の通りにコピペ。

ThreadClass.h
#pragma once
#include <memory>
class CThreadClass
{
public:
    // コンストラクタ
    CThreadClass();
    // デストラクタ
    virtual ~CThreadClass();
    // スレッドを開始する関数
    bool StartThread(CWnd* dialog_pointer);
    // スレッドを停止する関数
    bool StopThread() const;
    // 現在スレッドが動いているかどうか
    bool IsThread() const;
    // メインスレッド側に設定してある、ワーカーからメインへ送るメッセージを格納する。
    void SetMessageNumber(UINT thread_id) const;
    // ボタンに表示させる文字列(数字)を取得。
    // メインスレッドから呼び出される。
    // スレッドの動作そのものにはまったく関係の無いテスト用の関数。
    CString GetValue() const;
    // 動作テスト用の変数。スレッドの動作そのものには関係ない。
    int m_testNumber = 0;
protected:
    // ワーカースレッドが実行させる関数。
    void ThreadProc();
private:
    // Pimpl
    class Impl;
    std::unique_ptr<Impl> m_pimpl;
};

ThreadClassの定義も下記の通りにコピペ

ThreadClass.cpp
#include "stdafx.h"
#include "ThreadClass.h"

class CThreadClass::Impl
{
public:
    // スレッドの変数
    CWinThread* m_pThread;
    // スレッドを停止させるかどうかのbool値
    bool m_isTthread;
    // ワーカースレッドを実行させるためのstaticな関数。
    static UINT ThreadLauncher(LPVOID arg);
    // メインスレッドのメッセージID
    UINT m_threadId;
    Impl() : m_pThread(nullptr), m_isTthread(false), m_threadId(0){}
};

CThreadClass::CThreadClass() :m_pimpl(new Impl()) {} // NOLINT

CThreadClass::~CThreadClass() = default;

CString CThreadClass::GetValue() const
{
    CString val;
    val.Format(L"%d", m_testNumber);
    return val;
}

UINT CThreadClass::Impl::ThreadLauncher(const LPVOID arg)
{
    // ThreadProcをthisでそのまま実行させることが出来ないので、thisのポインタを設定してThreadProcを動作
    auto p_file_view = static_cast<CThreadClass *>(arg);
    if (p_file_view)
    {
        // ポインタから実行させる。
        p_file_view->ThreadProc();
    }
    return 0;
}

void CThreadClass::ThreadProc()
{
    // m_isTthreadがfalseになるまでワーカースレッドがループする
    while (m_pimpl->m_isTthread)
    {
        // ここにワーカースレッドが実行させる関数を書きます
        Sleep(1000);
        m_testNumber++;
        // ワーカースレッドからメインスレッドへメッセージを送ります。
        m_pimpl->m_pThread->m_pMainWnd->PostMessage(m_pimpl->m_threadId);
    }
}

bool CThreadClass::StartThread(CWnd* dialog_pointer)
{
    // スレッドが空かどうか
    if (m_pimpl->m_pThread != nullptr)
    {
        return false;
    }
    // スレッド登録
    m_pimpl->m_pThread = AfxBeginThread(m_pimpl->ThreadLauncher, LPVOID(this), THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED, nullptr);
    ASSERT(m_pimpl->m_pThread);
    if (m_pimpl->m_pThread)
    {
        // CTestMultiThreadDlgのインスタンスを登録
        m_pimpl->m_pThread->m_pMainWnd = dialog_pointer;
        // オートデリートON
        m_pimpl->m_pThread->m_bAutoDelete = TRUE;
    // この変数がtrueである限りワーカーが実行される。
        m_pimpl->m_isTthread = true;
        // ワーカー開始
        m_pimpl->m_pThread->ResumeThread();
    }
    return true;
}

bool CThreadClass::StopThread() const
{
    // スレッドが空かどうか。
    if (m_pimpl->m_pThread == nullptr)
    {
        return false;
    }
    // スレッドを停止命令
    m_pimpl->m_isTthread = false;
    // スレッド終了待ち(10秒待つ)
    if (WaitForSingleObject(m_pimpl->m_pThread->m_hThread, 10000) == WAIT_TIMEOUT)
    {
        // 終了待ちから指定時間たっても終了しない場合スレッド強制停止
        TerminateThread(m_pimpl->m_pThread->m_hThread, false);
        TRACE(L"スレッド強制停止");
    }
    // スレッドを空に
    m_pimpl->m_pThread = nullptr;
    return true;
}

bool CThreadClass::IsThread() const
{
    // スレッド自身が空かどうか
    if (m_pimpl->m_pThread == nullptr)
    {
        return false;
    }
    // スレッド停止命令が送られているか。
    return m_pimpl->m_isTthread;
}

void CThreadClass::SetMessageNumber(const UINT thread_id) const
{
    m_pimpl->m_threadId = thread_id;
}

0.o81ew4at77.PNG

以上

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8