前回作成したPrintMACを次のように変更してみましょう。
先ずは、Windows版からやってみましょう。
A:1秒ごとに50msec間隔で10個表示
B:2.5秒ごとに100msec間隔で5個表示
C:3.3秒ごとに200msec間隔で4個表示
timeGetTime()を使うためにstdThead.h, stdThread.cppを更新してください
#pragma comment(lib, “Winmm.lib”)が追加されています
【期待される出力例】 AAAAAAAAAA AAAAAAAAAA BBBBB AAAAAAAAAA CCCC AAAAAAAAAA
以下に変更点を列挙します。
1.CMyThreadに渡すパラメータに文字間表示間隔、表示文字数を追加
【MyThread.h】 // 必要なパラメータを構造体にする typedef struct { char cData; // 表示する文字 DWORD dwTimer; // 表示間隔(msec) DWORD dwCharacterInterval; // 文字間(msec) int iNum; // 表示する個数 } MyDataRec;
2.UINT CMyThread::DoWork()を仕様に合わせて変更
cDataをdwTimer(msec)ごとにdwCharacterInterval(msec)間隔でiNum個表示するように
変更します。
【コラム(経過時間を調べる関数】
timeGetTime()は32bits符号なし整数なので、2^32=49.71日で一周してしまいます。
経過時間を求めるときに、1周したときの処理が必要です。
ここではGetdwIntervalという関数で処理しています。
timeGetTime()の分解能は、timeBeginPeriod と timeEndPeriodで設定します。
分解能の設定例
timeBeginPeriod(1); // タイマーの最小精度を1msecにする
dwNowTime = timeGetTime();
timeEndPeriod(1); // タイマーの最小精度を戻す
他にGetTickCount()という関数も使えますが、分解能は15msec程度です。
timeGetTime()より精度を上げたいときはQueryPerformanceCounterを検討しましょう。
【MyThread.cpp】 UINT CMyThread::DoWork() { int ii; DWORD dwPrevTime = 0, dwNowTime; // 前回表示した時刻、現在時刻 while (!m_fStopFlag) { dwNowTime = timeGetTime(); // 現在時刻(Windowsの起動からのmsec) // 前回の表示時刻からdwTimer経過したか if (GetdwInterval(dwNowTime, dwPrevTime) >= m_pMyData->dwTimer) { // 指定された条件で文字を表示する for (ii = 0; ii < m_pMyData->iNum; ++ii) { fprintf(stderr, “%c”, m_pMyData->cData); Sleep(m_pMyData->dwCharacterInterval); } fprintf(stderr, “\n”); dwPrevTime = dwNowTime; // 表示した時刻を覚える } } return(1); }
3.CMyThreadのインスタンスの作成時に渡すパラメータを変更
【PrintMAC.cpp】 int main(int argc, char *argv[]) { MyDataRec MyData[3] = { { ‘A’, 1000, 50, 10 }, {‘B’, 2000, 100, 5}, {‘C’, 3300, 200, 4} }; CMyThread *pCMyThread[3] = { NULL }; int ii; for (ii = 0; ii < 3; ++ii) { pCMyThread[ii] = new CMyThread(&MyData[ii]); // パラメータ(構造体)のポインタを渡す pCMyThread[ii]->Begin(); // 開始 } getchar(); // Enterキー待ち for (ii = 0; ii < 3; ++ii) pCMyThread[ii]->End(); // スレッドに終了を通知 for (ii = 0; ii < 3; ++ii) { pCMyThread[ii]->WaitForEnd(); // 終了するのを待つ SAFE_DELETE(pCMyThread[ii]) // クラスのインスタンスの破棄 } return(0); }
実行してみましょう。
C:\PrintMACN\x64\Debug>PrintMAC CABAABACBAABAACBAA C AAAAAAAAAA ABABAABAABAABAA AAAAAACAAAAC CCABABA AABABAABAA
期待通りになっていません。
何が起こっているのでしょうか?
複数のスレッド(タスク)が共通のリソースに同時にアクセスするという事態が
発生しています。
共通のリソースとというのは、メモリ(グローバルな変数)やファイルです。
ここでは、標準エラー出力に、同時に3つのスレッドが書き込みを行っています。
UINT CMyThread::DoWork()のSleep(m_pMyData->dwCharacterInterval);
が呼び出されたときに、他のスレッドに実行が移っています。
共通のリソースへのアクセスを直列化(処理が終わるまで他のスレッドに実行させないように)
するのが、同期処理です。
Windowsの同期オブジェクトの種類には次のようなものがあります。
名前 速度 プロセス間同期 リソースカウント機能
クリティカルセクション 高 なし なし(排他アクセス)
ミューテックス 低 あり なし(排他アクセス)
セマフォ 低 あり あり
イベント 低 あり あり
メータードセクション 高 あり あり
ここでは、クリティカルセクションとミューテックスを使ってみます。
1.プロジェクト名:PrintMACCriticalSection
2.プロジェクト名:PrintMACMutex
PrintMACをコピーして、フォルダ名、ソリューション名、プロジェクト名を
変更をしてください。
最初にクリティカルセクションを使ってみます。
クリティカルセクションを所有しているスレッドのみが実行可能になる仕組みです。
CMyThreadに渡すパラメータの構造体のメンバにCRITICAL_SECTIONを追加します。
【MyThread.h】 typedef struct { char cData; // 表示する文字 DWORD dwTimer; // 表示間隔(msec) DWORD dwCharacterInterval; // 文字間(msec) int iNum; // 表示する個数 CRITICAL_SECTION *pcs; // 同期オブジェクト } MyDataRec;
クリティカルセクションのAPI
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
指定されたクリティカルセクションオブジェクトを初期化します
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
所有されていないクリティカルセクションオブジェクトを指定し、
そのオブジェクトが使っているすべてのリソースを解放します。
VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
指定されたクリティカルセクションオブジェクトの所有権を取得するまで待機します。
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
指定されたクリティカルセクションオブジェクトの所有権を解放します。
クリティカルセクションを初期化し、各スレッドに渡し、使用後開放する処理です。
変更箇所には★を付けました。
【PrintMAC.cpp】 int main(int argc, char *argv[]) { MyDataRec MyData[3] = { { ‘A’, 1000, 50, 10 }, {‘B’, 2000, 100, 5}, {‘C’, 3300, 200, 4} }; CMyThread *pCMyThread[3] = { NULL }; int ii; CRITICAL_SECTION cs; // ★クリティカルセクション InitializeCriticalSection(&cs); // ★クリティカルセクションの初期化 for (ii = 0; ii < 3; ++ii) // ★クリティカルセクションをパラメータにセット MyData[ii].pcs = &cs; for (ii = 0; ii < 3; ++ii) { pCMyThread[ii] = new CMyThread(&MyData[ii]); // パラメータ(構造体)のポインタを渡す pCMyThread[ii]->Begin(); // 開始 } getchar(); // Enterキー待ち for (ii = 0; ii < 3; ++ii) pCMyThread[ii]->End(); // スレッドに終了を通知 for (ii = 0; ii < 3; ++ii) { pCMyThread[ii]->WaitForEnd(); // 終了するのを待つ SAFE_DELETE(pCMyThread[ii]) // クラスのインスタンスの破棄 } DeleteCriticalSection(&cs); // ★クリティカルセクションの開放 return(0); }
各スレッドが実行する関数に同期処理を追加します。
変更箇所には★を付けました。
【MyThrtead.cpp】 UINT CMyThread::DoWork() { int ii; DWORD dwPrevTime = 0, dwNowTime; // 前回表示した時刻、現在時刻 while (!m_fStopFlag) { dwNowTime = timeGetTime(); // 現在時刻(Windowsの起動からのmsec) // 前回の表示時刻からdwTimer経過したか if (GetdwInterval(dwNowTime, dwPrevTime) >= m_pMyData->dwTimer) { EnterCriticalSection(m_pMyData->pcs); // ★所有権を取得するまで待つ // 指定された条件で文字を表示する for (ii = 0; ii < m_pMyData->iNum; ++ii) { fprintf(stderr, “%c”, m_pMyData->cData); Sleep(m_pMyData->dwCharacterInterval); } fprintf(stderr, “\n”); LeaveCriticalSection(m_pMyData->pcs); // ★所有権を開放する dwPrevTime = dwNowTime; // 表示した時刻を覚える } } return(1); }
これで完成です。
期待通りの動作をしていることを確認しましょう。
C:\PrintMACNCriticalSection\x64\Debug>PrintMACCriticalSection.exe BBBBB AAAAAAAAAA AAAAAAAAAA CCCC BBBBB AAAAAAAAAA CCCC AA
次にミューテックスを使ってみます。
ミューテックスのAPI
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // セキュリティ記述子
BOOL bInitialOwner, // 最初の所有者
LPCTSTR lpName // オブジェクトの名前
);
名前付きまたは名前なしのミューテックスオブジェクトを作成または開きます。
セキュリティ記述子:NULLを指定
最初の所有者:FALSEを指定(所有者なし)
オブジェクトの名前:NULLを指定(名前を指定すると、プロセスを渡って
有効なミューテックができる)
DWORD WaitForSingleObject(HANDLE hMutex, INFINITE);
所有権を取得するまで待ち続ける
BOOL ReleaseMutex(HANDLE hMutex);
指定されたミューテックスオブジェクトの所有権を解放します。
CMyThreadに渡すパラメータの構造体のメンバにミューテックスのハンドルを追加します。
【MyThread.h】 typedef struct { char cData; // 表示する文字 DWORD dwTimer; // 表示間隔(msec) DWORD dwCharacterInterval; // 文字間(msec) int iNum; // 表示する個数 HANDLE *phMutex; // 同期オブジェクト } MyDataRec;
ミューテックスを作成し、各スレッドに渡し、使用後破棄する処理です。
変更箇所には★を付けました
【PrintMAC.cpp】 int main(int argc, char *argv[]) { MyDataRec MyData[3] = { { ‘A’, 1000, 50, 10 }, {‘B’, 2000, 100, 5}, {‘C’, 3300, 200, 4} }; CMyThread *pCMyThread[3] = { NULL }; int ii; HANDLE hMutex; // ★ミューテックスのハンドル hMutex = CreateMutex(NULL, FALSE, NULL); // ★所有者のないミューテックスの作成 for (ii = 0; ii < 3; ++ii) // ★ミューテックスのハンドルをパラメータにセット MyData[ii].phMutex = &hMutex; for (ii = 0; ii < 3; ++ii) { pCMyThread[ii] = new CMyThread(&MyData[ii]); // パラメータ(構造体)のポインタを渡す pCMyThread[ii]->Begin(); // 開始 } getchar(); // Enterキー待ち for (ii = 0; ii < 3; ++ii) pCMyThread[ii]->End(); // スレッドに終了を通知 for (ii = 0; ii < 3; ++ii) { pCMyThread[ii]->WaitForEnd(); // 終了するのを待つ SAFE_DELETE(pCMyThread[ii]) // クラスのインスタンスの破棄 } CloseHandle(hMutex); // ★ミューテックスの破棄 return(0); }
各スレッドが実行する関数に同期処理を追加します。
変更箇所には★を付けました。
【MyThread.cpp】 UINT CMyThread::DoWork() { int ii; DWORD dwPrevTime = 0, dwNowTime; // 前回表示した時刻、現在時刻 while (!m_fStopFlag) { dwNowTime = timeGetTime(); // 現在時刻(Windowsの起動からのmsec) // 前回の表示時刻からdwTimer経過したか if (GetdwInterval(dwNowTime, dwPrevTime) >= m_pMyData->dwTimer) { WaitForSingleObject(*(m_pMyData->phMutex), INFINITE); // ★所有権を取得するまで待ち続ける // 指定された条件で文字を表示する for (ii = 0; ii < m_pMyData->iNum; ++ii) { fprintf(stderr, “%c”, m_pMyData->cData); Sleep(m_pMyData->dwCharacterInterval); } fprintf(stderr, “\n”); ReleaseMutex(*(m_pMyData->phMutex)); // ★所有権を解放 dwPrevTime = dwNowTime; // 表示した時刻を覚える } } return(1); }
これで完成です。
期待通りの動作をしていることを確認してください。
Linux版をやってみましょう。
Windowsの同期オブジェクトの種類には次のようなものがあります。
名前
ミューテックス
セマフォ
ここでは、ミューテックスを使ってみます。
プロジェクト名:PrintMACMutex
PrintMACをコピーして、フォルダ名、ソリューション名、プロジェクト名、makefile
の変更をしてください。
Linuxのmutexを取り扱うためのAPI
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
mutex オブジェクトを、 mutexattr で指定された mutex 属性オブジェクトに
従って初期化する。 mutexattr が NULL, ならば、デフォルトの属性が使われる。
常に 0 を返す
int pthread_mutex_destroy(pthread_mutex_t *mutex);
mutex オブジェクトを破壊し、それが保持している可能性のある資源を
開放する。mutex は関数の開始時点でアンロックされていなければならない。
成功すれば 0 を返し、 エラーでは非ゼロのエラーコードを返す
int pthread_mutex_lock(pthread_mutex_t *mutex);
指定したmutexをロックする。
mutex が現在ロックされていなければ、 それはロックされ、呼び出しスレッドに
よって所有される。この場合 pthread_mutex_lock は直ちに返る。
mutex が他のスレッドによって既にロックされていたのならば、 pthread_mutex_lockは
mutex がアンロックされるまで呼び出しスレッドの実行を停止させる。
成功すれば 0 を返し、 エラーでは非ゼロのエラーコードを返す
int pthread_mutex_unlock(pthread_mutex_t *mutex);
指定したmutexをアンロックする。
成功すれば 0 を返し、 エラーでは非ゼロのエラーコードを返す
CMyThreadに渡すパラメータの構造体のメンバにpthread_mutex_tを追加します。
【MyThread.h】 typedef struct { char cData; // 表示する文字 DWORD dwTimer; // 表示間隔(msec) DWORD dwCharacterInterval; // 文字間(msec) int iNum; // 表示する個数 pthread_mutex_t *pmutex; // 同期オブジェクト } MyDataRec;
ミューテックスを初期化し、各スレッドに渡し、使用後破棄する処理です。
変更箇所には★を付けました
【PrintMAC.cpp】 int main(int argc, char *argv[]) { MyDataRec MyData[3] = { { ‘A’, 1000, 50, 10 }, {‘B’, 2000, 100, 5}, {‘C’, 3300, 200, 4} }; CMyThread *pCMyThread[3] = { NULL }; int ii; pthread_mutex_t mutex; // ★ミューテックス pthread_mutex_init(&mutex, NULL); // ★ミューテックスの初期化 for (ii = 0; ii < 3; ++ii) MyData[ii].pmutex = &mutex; for (ii = 0; ii < 3; ++ii) { pCMyThread[ii] = new CMyThread(&MyData[ii]); // パラメータ(構造体)のポインタを渡す pCMyThread[ii]->Begin(); // 開始 } getchar(); // Enterキー待ち for (ii = 0; ii < 3; ++ii) pCMyThread[ii]->End(); // スレッドに終了を通知 for (ii = 0; ii < 3; ++ii) { pCMyThread[ii]->WaitForEnd(); // 終了するのを待つ SAFE_DELETE(pCMyThread[ii]) // クラスのインスタンスの破棄 } pthread_mutex_destroy(&mutex); // ★ミューテックスの破棄 return(0); }
各スレッドが実行する関数に同期処理を追加します。
変更箇所には★を付けました。
【MyThread.cpp】 UINT CMyThread::DoWork() { int ii; DWORD dwPrevTime = 0, dwNowTime; while (!m_fStopFlag) { dwNowTime = timeGetTime(); if (GetdwInterval(dwNowTime, dwPrevTime) >= m_pMyData->dwTimer) { pthread_mutex_lock(m_pMyData->pmutex); // ★所有権を取得するまで待つ for (ii = 0; ii < m_pMyData->iNum; ++ii) { fprintf(stderr, “%c”, m_pMyData->cData); usleep(m_pMyData->dwCharacterInterval * 1000); } fprintf(stderr, “\n”); pthread_mutex_unlock(m_pMyData->pmutex); // ★所有権を開放する dwPrevTime = dwNowTime; } } return(1); }
これで完成です。
期待通りの動作をしていることを確認してください。
pi@shimodaPi1:~/PrintMACMutex $ ./PrintMACMutex AAAAAAAAAA BBBBB CCCC AAAAAAAAAA AAAAAAAAAA BBBBB CCCC AAAAAAAAAA AAAAAAAAAA BBBBB AAAAAAAA
完成したプログラムを次のように変更して動作を確認してみてください
A:1秒ごとに50msec間隔で25個表示
B:2.5秒ごとに100msec間隔で5個表示
C:3.3秒ごとに200msec間隔で4個表示
CMyThreadに渡すパラメータの1か所を変更するだけです。
MyDataRec MyData[3] = { { ‘A’, 1000, 50, 25 }, {‘B’, 2000, 100, 5}, {‘C’, 3300, 200, 4} };
どうなりましたか。
pi@shimodaPi1:~/PrintMACMutex $ ./PrintMACMutex AAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAA AA ←ここでENTERキーを押しました。 CThreadFunc End CCC ThreadFunc End BBBBB ThreadFunc End
Aしか表示していません。
ENTERキーを押すと、BやCが表示されるので、スレッドはちゃんと動いていたようです。
表示する関数をもう一度みてみましょう。
UINT CMyThread::DoWork() { int ii; DWORD dwPrevTime = 0, dwNowTime; while (!m_fStopFlag) { dwNowTime = timeGetTime(); if (GetdwInterval(dwNowTime, dwPrevTime) >= m_pMyData->dwTimer) { pthread_mutex_lock(m_pMyData->pmutex); // ①所有権を取得するまで待つ for (ii = 0; ii < m_pMyData->iNum; ++ii) { fprintf(stderr, “%c”, m_pMyData->cData); usleep(m_pMyData->dwCharacterInterval * 1000); } fprintf(stderr, “\n”); pthread_mutex_unlock(m_pMyData->pmutex); // ②所有権を開放する dwPrevTime = dwNowTime; } } return(1); }
①でミューテックスの所有権を獲得して②で開放します。
②を実施後①が実施されるまでに、他のスレッドが所有権を獲得することができます。
{ ‘A’, 1000, 50, 25 }という条件で実施すると
①-②で、50*25=1250msecで1000msecをお超えているので、②-①の間がなくすぐに①
が実施されてしまいます。
そのため、他のスレッドが所有権を獲得することができないという事態が起こっています。
これを回避するには②にすぐ後にusleep(0);を書いて他のスレッドに実行のチャンスを
与えるようにします。
UINT CMyThread::DoWork() { int ii; DWORD dwPrevTime = 0, dwNowTime; while (!m_fStopFlag) { dwNowTime = timeGetTime(); if (GetdwInterval(dwNowTime, dwPrevTime) >= m_pMyData->dwTimer) { pthread_mutex_lock(m_pMyData->pmutex); // ①所有権を取得するまで待つ for (ii = 0; ii < m_pMyData->iNum; ++ii) { fprintf(stderr, “%c”, m_pMyData->cData); usleep(m_pMyData->dwCharacterInterval * 1000); } fprintf(stderr, “\n”); pthread_mutex_unlock(m_pMyData->pmutex); // ②所有権を開放する usleep(0); // ★他のスレッドに実行のチャンスを与える dwPrevTime = dwNowTime; } } return(1); }
期待通り動作することが確認できます。
pi@shimodaPi1:~/PrintMACMutex $ ./PrintMACMutex BBBBB AAAAAAAAAAAAAAAAAAAAAAAAA CCCC AAAAAAAAAAAAAAAAAAAAAAAAA BBBBB CCCC AAAAAAAAAAAAAAAAAAAAAAAAA BBBBB AAAAAAAAAAAAAAAAAAAAAAAAA CCC C BThreadFunc End BBBB AThreadFunc End AAAAAAAAAAAAAAAAAAAAAAAA ThreadFunc End
Windows版(CriticalSection, Mutex両方とも)oで同様に{ ‘A’, 1000, 50, 25 }とても
今回使用しているWindowsの環境では、この現象は発生しないようです。
念のためSleep(0)を入れておく方が良いかもしれません。
同期オブジェクトのAPIがWindowsとLinuxで異なっていたり、別の同期オブジェクトに
変更する、少し変更(Sleep(0)をいれるか、いれないか)ときに、メインのソースを変更
するのは大変なので使い方(API)を共通化するためのクラスを作成しましょう。
これについては次回ということで。
プロジェクトPrintMACCriticalSection for Windows