ネットワークプログラミングのデータの送受信は、下図のようにマルチスレッドで
実行します。
この節で、マルチスレッドプログラミングについて解説します。

次の動作をするプログラムを見てみましょう(シングルスレッド)
何かキーを入力されるまで文字’A’を1秒間隔で表示し続けるプログラム

【参考(Windows)】 #include “stdThread.h” #include >conio.h< BOOL CheckKey(); // キーボード入力のチェック関数 int main(int argc, char **argv) { while (1) { fprintf(stderr, “%c\n”, ‘A’); Sleep(1000); // 1000msec待つ if (CheckKey()) // キー入力があれば抜ける { break; } } return(0); } //////////////////////////////////////////////// // function // キー入力チェック // parameter // なし // return // TRUE:入力あり //////////////////////////////////////////////// BOOL CheckKey() { BOOL fRet = FALSE; if (_kbhit()) { _getch(); fRet = TRUE; } return(fRet); }

特に難しいところはないと思います。
LinuxではSleepは秒単位ですので、uspleepマイクロ秒単位を使います。
CheckKeyはselectを使ったものに置き換えます。
次のような感じですね。

【参考(Linux)】 #include “stdThread.h” BOOL CheckKey(); // キーボード入力のチェック関数 int main(int argc, char **argv) { while (1) { fprintf(stderr, “%c\n”, ‘A’); usleep(1000 * 1000); // 1000msec待つ if (CheckKey()) // ENTERキー入力があれば抜ける { break; } } return(0); } //////////////////////////////////////////////// // function // キー入力チェック // parameter // なし // return // TRUE:入力あり //////////////////////////////////////////////// BOOL CheckKey() { BOOL fRet = FALSE; fd_set rfds; struct timeval tv; char szBuff[81]; tv.tv_sec = 0; tv.tv_usec = 0; FD_ZERO(&rfds); // fd_setの初期化 // 0:標準入力 1:標準出力 2:標準エラー FD_SET(0, &rfds); // 標準入力が調査対象 select(1, &rfds, NULL, NULL, &tv); if (FD_ISSET(0, &rfds)) // キー入力があった { read(0, szBuff, sizeof(szBuff)); fRet = TRUE; } return(fRet); }

さて、このプログラムを
「何かキーを入力されるまで文字’A’を1秒間隔で’B’を2秒間隔で表示し続ける
プログラム」に変更するとするとどうでしょう。
また「何かキーを入力されるまで文字’A’を1秒間隔で’B’を2秒間隔で’C’を2.5秒
間隔で表示し続けるプログラム」ではどうでしょう。
カウンタを用意してSleep、usleepの時間を調整して、待った回数で表示する文字を
変えるとできそうですね。
「’C’を3.3秒間隔で」にすると、また面倒になりますね。
ちょっと、仕様を変更するだけで、プログラム全体を変更しなければなりませんし
どんどんロジックだ複雑になってしまします。

そこで、マルチタスクを考えてみましょう。
単純なジョブを複数同時に実行できると上記のようなプログラムを簡単に
実現できそうです。
ここでは単純なジョブは、次のようなものです。
指定された文字を一定間隔で表示する。
例えば
Func(‘A’, 1000);
Func(‘B’, 2500);
Func(‘C’, 3300);
って書くと3つの関数が同時に実行されるという感じです。

マルチタスクを実現するには次のような方法があります。
マルチプロセス
Linuxにはfork()という関数がありマルチプロセスを
実現することができます(気になる方はサンプル(一部)があります)
親プロセスのメモリ、スタック、ファイルディスクリプタ、
などすべての状態の複製を作成してくれます。(とても簡単)
ただし、複製をつくり、プロセスを起動するコストはかかります。

WindowsにはCreateProcess()という関数がありますがこれは
単にプログラムを実行する関数なので使えません。

マルチスレッド
同じプロセスで複数の処理を実行することができます。
処理を関数にまとめて、実行するように指示します(なれれば簡単)
プロセスを起動するコストはかかりません。
Windows、Linuxの両方にこの機能を実現する仕組みがあります。
ここではWindows、Linuxの両方で使えるマルチスレッドを採用しましょう。

マルチスレッドプログラム(Windows)

Windowsマルチスレッドを実装するための関数に次のようなものがあります。
beginthread(), beginthreadex(), CreateThread(), AfxBeginThread(),
違いを見てみましょう。
CreateThread、ExitThread:Win32API
CloseHandle必要
マルチスレッド対応ランタイムライブラリを使用できない(使用時は同期をかける)
理由:メモリリークが発生する場合がある。
例:asctime()、ctime()、localtime()、gmtime()、mktime()、errno
Win32API関数のみ使用。
うーん、使い難そうですね。

beginthread、endthread:マルチスレッド対応ライブラリ
CloseHandle不要(自動的にスレッドハンドルを閉じます)
マルチスレッド対応ランタイムライブラリを使える
スレッドIDは取得できない。
使えそうです。

beginthreadex、endthreadex:マルチスレッド対応ライブラリ
CloseHandle必要
マルチスレッド対応ランタイムライブラリを使える
スレッドIDを取得できる
これは、お薦めの関数ですね。ここではこれを使うことにしましょう

AfxBeginThread、AfxEndThread:MFCライブラリ
マルチスレッド対応ランタイムライブラリを使える
これは、お薦めの関数ですね。

使用する関数
uintptr_t _beginthreadex(void *security, unsigned stack_size,
unsigned ( __stdcall *start_address )( void * ), void *arglist,
unsigned initflag, unsigned *thrdaddr);

スレッドを作成します。
成功すると、新しく作成されたスレッドのハンドルを返します。
エラーが発生すると、0 を返します
Security:SECURITY_ATTRIBUTES 構造体へのポインター(NULL)
stack_size:新しいスレッドのスタック サイズまたは 0(0)
start_address:新しいスレッドの実行を開始するルーチンの開始アドレス(関数名)
arglist:新しいスレッドに渡される引数リストまたは NULL
initflag:即時に実行するには initflag を 0 に設定
一時停止状態でスレッドを作成するには CREATE_SUSPENDED に設定
スレッドを実行するには、 ResumeThread を使用
thrdaddr:スレッド識別子を受け取る 32 ビット変数へのポインター

void _endthreadex(unsigned retval);
スレッドを終了します。
retval:スレッド終了コード
この関数は、ルーチンからスレッドが戻ると自動的に呼び出されます。
つまり省略可なのですが、コールするとスレッドに割り当てられていたリソースが
確実に解放されるので呼び出すほうが良いです。

BOOL GetExitCodeThread(HANDLE hThread, LPDWORD lpExitCode);
スレッドの終了コードを取得します。
hThraed:対象とするスレッドのハンドル
lpExitCode:終了コードを格納するバッファのポインタ

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
スレッドが終了したかを検査します。
次のいずれかが成立すると、制御を返します。
指定されたオブジェクトがシグナル状態になった
タイムアウト時間が経過した
hHandle:対象とするスレッドのハンドル
dwMilliseconds:タイムアウト時間

それでは文字を表示するプログラムのマルチスレッド対応版を作ってみましょう。
「ファイル」→「新規作成」→「VisualC++」→「空のプロジェクト」
「ソリューションのディレクトリを作成する」のチェックを外します。
「場所」は各自が読み書きできるフォルダを指定します。
「名前」はPrintMAとします。
プロジェクト→既存の項目の追加を選びstdThread.cpp, stdThread.hを追加します。
_beginthreadex, _endthreadex を使用するには、<process.h>をインクルードします。
stdThread.hでインクルードしていることを確認してください。

プロジェクト→新規の項目の追加を選びPrintMA.cppを追加します。
プロジェクト→プロパティの構成プロパティの文字セットが「マルチ バイト文字セットを
使用する」になっていることを確認してください。
プラットフォームは64bits(x64)にしましょう。

スレッドで実行する関数に渡すパラメータはvoid *arglistなので
複数のパラメータは一つの構造体に格納して、そのポインタを渡すことにします。
修了を通知するフラグgfStopFlagに修飾子volatileを指定しますが、これに
ついては後程説明します。

【PrintMA.cpp】 #include “stdThread.h” // スレッドで実行する関数に渡すパラメータ用構造体 typedef struct { char cData; DWORD dwTimer; }MyDataRec; unsigned int __stdcall Func(void *pvoid); // スレッドで実行する関数の型宣言 volatile BOOL gfStopFlag; // 修了を通知するためのフラッグ int main(int argc, char *argv[]) { HANDLE hThread; unsigned int threadID; MyDataRec MyData = { ‘A’, 1000 }; DWORD dwRet; gfStopFlag = FALSE; hThread = (HANDLE)_beginthreadex(NULL, 0, Func, &MyData, 0, &threadID); getchar(); // ENTERキーが入力されるのを待つ gfStopFlag = TRUE; // 終了を通知 while (1) // スレッドで実行した関数の終了を待つ { if (WaitForSingleObject(hThread, 50) == WAIT_OBJECT_0) { GetExitCodeThread(hThread, &dwRet); // スレッド関数からの戻り値取得 fprintf(stderr, “Thread ended. endcode:%d\n”, dwRet); CloseHandle(hThread); break; } } return(0); } unsigned int __stdcall Func(void *pvoid) { MyDataRec *pData = (MyDataRec *)pvoid; unsigned int dwRet = 0; while (!gfStopFlag) { printf(“%c\n”, pData->cData); Sleep(pData->dwTimer); } dwRet = 1; _endthreadex(dwRet); // 返す値はこの関数に渡す return(0); // _endthreadexを書くならここへは来ない }

volatile型修飾子について
初期のCであるK&Rには含まれていない
ANSI C(C89)以降のC標準規格にはconstと一緒に含まれる
処理系で行われる最適化を抑止する
下図のような最適化(外部変数event_flagの評価を最適化)が行われると、
フラグevent_flagを外部から1に変えてもループから抜け出さないので、
volatileを記述して最適化を抑制します。

マルチスレッドプログラム(Linux)

使用する関数
int pthread_create(pthread_t *thread, pthread_attr_t *attr,
void * (*start_routine)(void *), void *arg);

新しいスレッドを生成する
成功すると0 が返る
thread:新しく作成したスレッドの識別子の格納エリアのポインタ
attr:その新しいスレッドに適用するスレッド属性(NULL)
start_routine:新しいスレッドの実行を開始するルーチンの開始アドレス(関数名)
arg:新しいスレッドに渡される引数リストまたは NULL

void pthread_exit(void *retval);
呼び出しスレッドを終了する
retval:スレッドの終了値、使用時はエリアを確保して渡す

int pthread_join(pthread_t th, void **thread_return);
呼び出しスレッドの実行を停止し、 th で指定したスレッドが終了するのを待つ
この呼び出してリソースが解放される
th:対象とするスレッドの識別子
thread_return:th の終了値が格納されるエリアのポインタ

それでは文字を表示するプログラムのマルチスレッド対応版を作ってみましょう。
ファイル→新規作成→クロスプラットフォーム→メイクファイルプロジェクト
プロジェクト名:PrintMA
「ソリューションのディレクトリを作成する」のチェックを外します。
stdThread.cpp, stdThread.hをリンクから取得し、既存の項目として追加します。
makefileをSimpleClientなどからコピーし、既存の項目として追加します。
makefileを変更します。
PrintMA.cppを新規に作成します。
プロパティの設定は、ここを参考に行ってください

マルチスレッド関数はライブラリpthreadを使用するのでビルドは次のように行います。
g++ -oPrintMA PrintMA.cpp -lpthread
これまで使用してきたmakefileでは、上記のようになっています。
確認していください。
<pthread.h>をインクルードします。
stdThread.hでインクルードしていることを確認してください。

【makefile】 CC=g++ -g -O0 #CC=g++ PROGRAM=PrintMA OBJS=PrintMA.o SRCS=$(OBJS:%.o=%.cpp) INCLUDE=stdThread.h LFLAGS=-lpthread $(PROGRAM):$(OBJS) $(SRCS) $(INCLUDE) $(CC) -o $(PROGRAM) $(SRCS) $(LFLAGS)

【PrintMA.cpp】 #include “stdThread.h” // スレッドで実行する関数に渡すパラメータ用構造体 typedef struct { char cData; DWORD dwTimer; }MyDataRec; void *Func(void *pvoid); // スレッドで実行する関数の型宣言 volatile BOOL gfStopFlag; // 終了を通知するためのフラッグ int main(int argc, char *argv[]) { int iHandle; pthread_t idThread; MyDataRec MyData = { ‘A’, 1000 }; void *pResult = NULL; gfStopFlag = FALSE; iHandle = pthread_create(&idThread, NULL, Func, &MyData); getchar(); // ENTERキーが入力されるのを待つ gfStopFlag = TRUE; // 終了を通知 pthread_join(idThread, &pResult); // スレッドで実行した関数の終了を待つ // スレッド関数からの戻り値取得 fprintf(stderr, “Thread ended. endcode:%d\n”, *(DWORD *)pResult); SAFE_FREE(pResult) return(0); } void *Func(void *pvoid) { MyDataRec *pData = (MyDataRec *)pvoid; DWORD *pdwResult = (DWORD *)calloc(1, sizeof(DWORD)); while (!gfStopFlag) { fprintf(stderr, “%c\n”, pData->cData); usleep(pData->dwTimer * 1000); } *pdwResult = 1; pthread_exit(pdwResult); // 返す値はこの関数に渡す return(NULL); // pthread_exitを書くならここへは来ない }

これで、マルチスレッドプログラムの入門編は完了です。
使い方がわかれば、手軽にマルチスレッドを実現できることがわかりました。
WindowsとLinuxでスレッドで実行する関数の型が異なったり、開始・終了の
仕方が違ったりしますので、このあたりを共通化するための基底クラスを
作成しましょう。
これについては次回ということで。

プロジェクトPrintMA for Windows

プロジェクトPrintMA for Linux