マルチスレッドプログラムが難しいと感じてしまう点について考えてみましょう
次のようなシナリオを想定してみてください。
別の機能(例えばTCP/IPの送受信)を実現する
Windows版、Linux版の両方のプログラムを作成する
機能を実現する度にマルチスレッド実現部分から作らなければならなりません。
WindowsとLinuxでマルチスレッド部分の実行の仕方が違うので
もし、マルチスレッド実現部分を独立させ簡単につかえれば、
実現したい機能のみに集中できるようになります。
例としてここでは、文字表示プログラムをマルチスレッドを実現する基本クラスからの
派生クラスによって実現してみましょう。
オブジェクト指向(クラス)を使ってマルチスレッドを実現
マルチスレッドを実現する基本クラス、CThreadJobを作成します
これにより、ジョブの実行関数、実行、停止などが使いやすくなります。
また、これを使うことでWindowsとLinuxとのソースレベルでの互換性が
かなり図れます。
【基本クラス(CThreadJob)のイメージ】 メソッド, プロパティ BOOL Begin(); // スレッドを開始する void End(); // スレッドを停止 DWORD WaitForEnd(); // 停止するまで待つ // 派生クラスの実行内容を記述したDoWorkでオーバライド virtual UINT DoWork() { return 0; }; protected: volatile BOOL m_fStopFlag; // 終了用フラグ
各機能(例 指定された文字を指定した間隔で表示する)は派生クラスとして実現します。
マルチスレッドの機能はCThreadJobに実装済なので、派生クラスでは
マルチスレッドを “ほとんど” 意識しなくて済むようになります。
【派生クラス(CMyThraed)のイメージ】 class CMyThread : public CThreadJob // パラメータは実現する機能に必要な情報を格納する構造体を定義し、 // そのポインタをコンストラクタで渡す // 渡されたパラメータpDataはコンストラクタで覚える CMyThread(MyDataRec *pData); Begin()は、CThreadJobに実装済みなので派生クラスに記述する必要なし End()は、CThreadJobに実装済みなので派生クラスに記述する必要なし // DoWork()に機能を記述する UINT DoWork() override;
【使い方のイメージ】 // 必要な情報を構造体にセットする MyDataRec MyData = { ‘A’, 1000 }; CMyThread *pCMyThread = NULL; // 派生クラスのインスタンスを作成する // 必要な情報を引数として渡す pCMyThread = new CMyThread(&MyData); // スレッドの開始 pCMyThread->Begin(); getchar(); // スレッドに終了を通知 pCMyThread->End(); // スレッドの終了を待つ pCMyThread->WaitForEnd(); // クラスのインスタンスの破棄 delete(pCMyThread);
CThreadJob(Windows版)をPrintM.cppを参照してマルチスレッド部分を抽出して作成します。
ThreadJob.hで宣言している(派生クラスで)ジョブを記述する関数の型が_beginthreadexで
指定する呼びたし関数の型と無関係になっていることに注意しておいてください。
【ThreadJob.h】 #pragma once // 多重にincludeされないための記述 #include “stdThread.h” class CThreadJob { public: CThreadJob(); ~CThreadJob(); BOOL Begin(); // スレッドを開始する void End(); // スレッドを停止 DWORD WaitForEnd(); // 停止するまで待つ // 派生クラスの実行内容を記述したDoWorkでオーバライド virtual UINT DoWork() { return 0; }; private: HANDLE m_hThread; // スレッドハンドル 終了待ちに使用 protected: volatile BOOL m_fStopFlag; // 終了用フラグ };
それではThreadJob.cppに関数を実装しましょう。
_beginthreadexに渡す関数は、クラスのインスタンス関数ではないので
グローバル(クラスのインスタンスとは無関係)に配置されるように記述します。
この関数内で、クラスのインスタンスを使いたいので、引数としてはクラスの
インスタンスのポインタ(this)を渡します。
【ThreadJob.cpp】 #include “ThreadJob.h” // _beginthreadexに渡す関数 // グローバル(クラスのインスタンスとは無関係)に配置されます // 当然クラスのメンバにはアクセスできないので // クラスのインスタンス(this)をパラメータとして渡して呼び出す unsigned int __stdcall ThreadProc(void *pvoid); //////////////////////////////////////////////// // function // コンストラクタ // parameter // なし // return // なし //////////////////////////////////////////////// CThreadJob::CThreadJob() { // メンバ変数の初期化 m_hThread = 0; m_fStopFlag = FALSE; } //////////////////////////////////////////////// // function // デストラクタ // parameter // なし // return // なし //////////////////////////////////////////////// CThreadJob::~CThreadJob() { } //////////////////////////////////////////////// // function // スレッド開始 // parameter // なし // return // TRUE/FALSE //////////////////////////////////////////////// BOOL CThreadJob::Begin() { UINT threadID; BOOL fRet; m_fStopFlag = FALSE; // ThreadProcがクラスメンバにアクセスできるように // クラス(実際は派生クラス)のインスタンスのポインタを引数として渡す m_hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, this, 0, &threadID); fRet = (m_hThread == 0) ? FALSE : TRUE; return(fRet); } //////////////////////////////////////////////// // function // スレッドへ終了を通知 // parameter // なし // return // なし //////////////////////////////////////////////// void CThreadJob::End() { m_fStopFlag = TRUE; } //////////////////////////////////////////////// // function // スレッドの終了を待つ // parameter // なし // return // スレッドで実行した関数の戻り値 //////////////////////////////////////////////// DWORD CThreadJob::WaitForEnd() { DWORD dwRet; if (m_hThread) { while (1) { if (WaitForSingleObject((HANDLE)m_hThread, 50) == WAIT_OBJECT_0) { GetExitCodeThread(m_hThread, &dwRet); CloseHandle((HANDLE)m_hThread); m_hThread = 0; break; } } } return(dwRet); } //////////////////////////////////////////////// // function // 別スレッドで実行する関数 // 派生クラスでオーバライドされるDoWorkを呼び出す // parameter // void *pvoid [in]CThreadJobのインスタンス // return // 実行結果 //////////////////////////////////////////////// unsigned int __stdcall ThreadProc(void *pvoid) { unsigned int uRet; CThreadJob *pCThreadJob = (CThreadJob *)pvoid; uRet = pCThreadJob->DoWork(); fprintf(stderr, “ThreadFunc End\n”);// _endthreadexより前に書かないと実行されません _endthreadex(uRet); // _endthreadexを使うときはこの引数が戻り値となる return(0); }
指定された文字を指定された間隔で表示する派生クラス(CMyThread)を作成して
PrintMAと同じ動作をするプログラムを作成しましょう。(Windows編)
「ファイル」→「新規作成」→「VisualC++」→「空のプロジェクト」
「ソリューションのディレクトリを作成する」のチェックを外します。
「場所」は各自が読み書きできるフォルダを指定します。
「名前」はPrintMACとします。
プロジェクト→既存の項目の追加を選びstdThread.cpp, stdThread.hを追加します。
上で作成したTheadJob.h, ThreadJob.cppを同様に追加します。
プロジェクト→新規の項目の追加を選びPrintMAC.cppを追加します。
プロジェクト→新規の項目の追加、C++のクラスを選びます。
クラス名:CMyThread ファイル:MyThread.h, MyThread.cpp
基底クラス:CThreadJob
プロジェクト→プロパティの構成プロパティの文字セットが「マルチ バイト文字セットを
使用する」になっていることを確認してください。
プラットフォームは64bits(x64)にしましょう。
MyThread.hです。
CThreadJobからの派生クラスになっています。(class CMyThread : public CThreadJob)
このファイルには、Windows独自の記述がありません。
つまり、Linuxとソースレベルで完全に互換です。
【MyThread.h】 #pragma once #include “ThreadJob.h” // 必要なパラメータを構造体にする typedef struct { char cData; // 表示する文字 DWORD dwTimer; // 表示間隔(msec) } MyDataRec; class CMyThread : public CThreadJob { public: CMyThread(MyDataRec *pData); // パラメータをコンストラクタで渡す ~CMyThread(); // 基底クラスの関数をオーバーライドする // C++11で明示的にoverrideを書くことが出来るようになりました // 基底クラスの当該関数にvirtualが書いていないとエラーを出してくれます UINT DoWork() override; // cDataをdwTime(msec)間隔で表示する private: MyDataRec *m_pMyData; // コンストラクタで渡されるパラメータを格納 // このスレッド実行中領域が確保されていること };
MyThread.cppです。
Windows独自の記述はSleepだけです。
【MyThread.cpp】 #include “MyThread.h” //////////////////////////////////////////////// // function // コンストラクタ // parameter // MyDataRec *pData [in]機能に必要な情報 // return // なし //////////////////////////////////////////////// CMyThread::CMyThread(MyDataRec *pData) { m_pMyData = pData; } //////////////////////////////////////////////// // function // デストラクタ // parameter // なし // return // なし //////////////////////////////////////////////// CMyThread::~CMyThread() { } //////////////////////////////////////////////// // function // 機能を記述した関数 // parameter // なし // return // 0(特にエラーもないので) //////////////////////////////////////////////// UINT CMyThread::DoWork() { while (!m_fStopFlag) { fprintf(stderr, “%c\n”, m_pMyData->cData); Sleep(m_pMyData->dwTimer); } return(1); // 返り値として1を返しています }
PrintMAC.cppです。
Windows独自の記述はありません。
非常にコードも見やすくなっていると思います。
【PrintMAC.cpp】 #include “MyThread.h” int main(int argc, char *argv[]) { MyDataRec MyData = { ‘A’, 1000 }; CMyThread *pCMyThread = NULL; pCMyThread = new CMyThread(&MyData); // パラメータ(構造体)のポインタを渡す pCMyThread->Begin(); // 開始 getchar(); // Enterキー待ち pCMyThread->End(); // スレッドに終了を通知 pCMyThread->WaitForEnd(); // 終了するのを待つ SAFE_DELETE(pCMyThread) // クラスのインスタンスの破棄 return(0); }
Linux版PrintMACを作ってみましょう。
ThreadJob.h, ThreadJob.cppについてもWindows版を参考にソースを見てもらうと
理解することができると思います。
ファイル→新規作成→クロスプラットフォーム→メイクファイルプロジェクト
プロジェクト名:PrintMAC
「ソリューションのディレクトリを作成する」のチェックを外します。
stdThread.cpp, stdThread.hをリンクから取得し、既存の項目として追加します。
makefileをSimpleClientなどからコピーし、既存の項目として追加します。
ThreadJob.h, ThreadJob.cpp, MyThread.h, MyThread.cpp, PrintMAC.cppを
新規の項目として追加します。
makefileを変更します。
プロパティの設定は、ここを参考に行ってください。
【makefile】 CC=g++ -g -O0 #CC=g++ PROGRAM=PrintMAC OBJS=PrintMA.o ThreadJob.o MyThread.o SRCS=$(OBJS:%.o=%.cpp) INCLUDE=stdThread.h LFLAGS=-lpthread $(PROGRAM):$(OBJS) $(SRCS) $(INCLUDE) $(CC) -o $(PROGRAM) $(SRCS) $(LFLAGS)
ThreadJob.hです。
違いはスレッドの終了を待つために必要なのがハンドルから
スレッド識別子になっているところだけです。
【ThreadJob.h】 #pragma once #include “stdThread.h” class CThreadJob { public: CThreadJob(void); ~CThreadJob(void); BOOL Begin(); // スレッドを開始する void End(); // スレッドを停止 DWORD WaitForEnd(); // 停止するまで待つ // 派生クラスの実行内容を記述したDoWorkでオーバライド virtual UINT DoWork() { return 0; }; private: pthread_t m_idThread; protected: volatile BOOL m_fStopFlag; };
ThreadJob.cppです。
Windows版との違いはスレッドで実行する関数の型、マルチスレッドの開始、終了をする関数、
終了を待つためにスレッド識別子を覚えるというところです。
PintMAで作ったものをクラスのメソッドとして記述し直しているだけです。
【ThreadJob.cpp】 #include “ThreadJob.h” // pthread_createに渡す関数 // グローバル(クラスのインスタンスとは無関係)に配置されます // 当然クラスのメンバにはアクセスできないので // クラスのインスタンス(this)をパラメータとして渡して呼び出す void *ThreadFunc(void *pvoid); //////////////////////////////////////////////// // function // コンストラクタ // parameter // MyDataRec *pData [in]機能に必要な情報 // return // なし //////////////////////////////////////////////// CThreadJob::CThreadJob(void) { // メンバ変数の初期化 m_idThread = 0; m_fStopFlag = FALSE; } //////////////////////////////////////////////// // function // デストラクタ // parameter // なし // return // なし //////////////////////////////////////////////// CThreadJob::~CThreadJob(void) { } //////////////////////////////////////////////// // function // スレッド開始 // parameter // なし // return // TRUE/FALSE //////////////////////////////////////////////// BOOL CThreadJob::Begin() { int iRet; BOOL fRet; m_fStopFlag = FALSE; // ThreadProcがクラスメンバにアクセスできるように // クラス(実際は派生クラス)のインスタンスのポインタを引数として渡す iRet = pthread_create(&m_idThread, NULL, ThreadFunc, this); fRet = (iRet != 0) ? FALSE : TRUE; return(fRet); } //////////////////////////////////////////////// // function // スレッドへ終了を通知 // parameter // なし // return // なし //////////////////////////////////////////////// void CThreadJob::End() { m_fStopFlag = TRUE; } //////////////////////////////////////////////// // function // スレッド終了を待つ // parameter // なし // return // スレッドで実行した関数の戻り値 //////////////////////////////////////////////// DWORD CThreadJob::WaitForEnd() { DWORD dwRet; void *pResult; pthread_join(m_idThread, &pResult); dwRet = *(DWORD *)pResult; SAFE_FREE(pResult) return(dwRet); } //////////////////////////////////////////////// // function // 別スレッドで実行する関数 // 派生クラスでオーバライドされるDoWorkを呼び出す // parameter // void *pvoid [in]CThreadJobのインスタンス // return // 実行結果 //////////////////////////////////////////////// void *ThreadFunc(void *pvoid) { DWORD *pdwResult = (DWORD *)calloc(1, sizeof(DWORD)); // 返す値はエリアを確保 CThreadJob *pCThreadJob = (CThreadJob *)pvoid; *pdwResult = pCThreadJob->DoWork(); printf(“ThreadFunc End\n”); // pthread_exitより前に書かないと実行されません pthread_exit(pdwResult); // 返す値はこの関数に渡す return(NULL); }
MyThread.hです。
これはWindows版と全く同じです。
【MyThread.h】 #pragma once #include “ThreadJob.h” // 必要なパラメータを構造体にする typedef struct { char cData; // 表示する文字 DWORD dwTimer; // 表示間隔(msec) } MyDataRec; class CMyThread : public CThreadJob { public: CMyThread(MyDataRec *pData); // パラメータをコンストラクタで渡す ~CMyThread(); // 基底クラスの関数をオーバーライドする // C++11で明示的にoverrideを書くことが出来るようになりました // 基底クラスの当該関数にvirtualが書いていないとエラーを出してくれます UINT DoWork() override; // cDataをdwTime(msec)間隔で表示する private: MyDataRec *m_pMyData; // コンストラクタで渡されるパラメータを格納 // このスレッド実行中領域が確保されていること };
MyThread.cppです。
Windows版との違いはSleepではなく、usleepを使っているところだけです。
【MyThread.cpp】 #include “MyThread.h” //———————————————- // function // コンストラクタ // parameter // MyDataRec *pData [in]機能に必要な情報 // return // なし //———————————————- CMyThread::CMyThread(MyDataRec *pData) { m_pMyData = pData; } //———————————————- // function // デストラクタ // parameter // なし // return // なし //———————————————- CMyThread::~CMyThread() { } //———————————————- // function // 機能を記述した関数 // parameter // なし // return // 0(特にエラーもないので) //———————————————- UINT CMyThread::DoWork() { while (!m_fStopFlag) { fprintf(stderr, “%c\n”, m_pMyData->cData); usleep(m_pMyData->dwTimer * 1000); } return(1); }
最後にPrintMAC.cppです。
もちろんWindows版との違いありません。
【PrintMAC.cpp】 #include “MyThread.h” int main(int argc, char *argv[]) { MyDataRec MyData = { ‘A’, 1000 }; CMyThread *pCMyThread = NULL; pCMyThread = new CMyThread(&MyData); pCMyThread->Begin(); getchar(); pCMyThread->End(); pCMyThread->WaitForEnd(); delete(pCMyThread); return(0); }
基本クラスCThredJobを使うことで、マルチスレッドの機能の実現は
これに任すことができます。
これの派生クラス(DoWork関数)に、機能を記述しますが、WindowsとLinuxの
ソースレベルの互換性がかなり実現されます。
main部分の互換性はもっと実現されていることがわかりました。
このクラスを使ってTCP/IPの送受信の機能をマルチスレッド化することにしましょう。
その前に、マルチスレッドを考えるときに必要な同期処理について、
次回触れることにします。