スレッドの同期(セマフォの使い方)

前回、スレッドを一つつくるプログラムを紹介しました。今回は、スレッドを複数にして動作させる方法について解説します。そして、複数動作させると変数の取り合いが問題になります。そこで、同期方法セマフォという考え方についても解説します。

スレッドを複数にする

では、スレッドを10個動作させてみます。つまり、OKをおした動作を

void CMFCApplication1Dlg::OnBnClickedOk()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	//CDialogEx::OnOK();
	thread_id = 0;
	count = 0;
	for (int i = 0; i < 10; i++){
		AfxBeginThread(MyThread, this);
	}
}

として実行します。for文でAfxBeginThreadを10回起動しています。Threadが10個でき10回countをカウントするので、最終的に

Thread:9, count: 99

と表示されるはずです。動作させると、最終的に

Thread:9, count:97

が表示されました。これは明らかにおかしいです。何が起こっているのでしょうか?

スマホが1つなら使う人も1人

原理は簡単で、一つしかない変数countに同時に複数のスレッドがアクセスしているためです。スマホが1台なら、使う人も1しか無理です。100人使用することは不可能です。これと同じで、countに複数のスレッドが集中し、誤ってcountが97となったわけです。

スレッドの同期

以上のような変数の取り合いがないようにスレッドを同期させる必要があります。同期には

  1. セマフォ
  2. イベントオブジェクト
  3. クリティカルセクション

の4種類があります。ここではセマフォとミューテックスセマフォにについて解説します。イベントオブジェクトとクリティカルセクションは次回解説します。

セマフォ

セマフォの考えかたは単純で、貸出をきちんと管理しようというものです。例えば、スマフォが1台しかない場合、スマフォの数を管理しておきます。誰かがスマフォを借りにきた場合、このスマフォの数を確認します。今は1台あるので貸し出します(Lock)。そして、また別の人がスマフォを借りに来た場合は、スマフォが0台しかないので返却が済む(UnLock)まで待ってもらいます。では実際にセマフォを使ってみましょう。

セマフォを使うにはMFCApplication1Dlgのプロパティに

CSemaphore sema;

と追加します。そして、semaのコンストラクタの引数でセマフォの使用回数を決定したいので、

CMFCApplication1Dlg::CMFCApplication1Dlg(CWnd* pParent /*=NULL*/)
	: CDialogEx(CMFCApplication1Dlg::IDD, pParent)
	, sema(1,1)
{
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

とコンストラクタの初期化を指定します。ここで、第一引数は初期のセマフォの使用可能数で、第二引数はセマフォの最大数です。この場合は、両方とも1を指定しています。

セマフォのインスタンスをつくります。次にスレッド用関数を

UINT MyThread(LPVOID p){
	CMFCApplication1Dlg* ptr = (CMFCApplication1Dlg*)p;
	CString ss;
	int id = ptr->thread_id;
	CSingleLock SyncObj(&(ptr->sema));
	SyncObj.Lock();
	ptr->thread_id++;
	SyncObj.Unlock();
	for (int i = 0; i < 10; i++){
		if (ptr->flag_end) break;
		ss.Format(_T("Thread:%d, count:%d"), id, ptr->count);
		ptr->m_label.SetWindowTextW(ss);
		SyncObj.Lock();
		ptr->count++;
		SyncObj.Unlock();
		Sleep(500);
	}
	return 0;
}

とします。ここでは、thread_idとcountに競合が起きないようにセマフォで制御しています。Lock命令をするとセマフォの数が一つ減ります。もしセマフォの数が0の場合は増えるまでスレッドは待機します。Unlockで返却です。

あとはスレッドを実行させるだけです。OKボタンの動作を以下のように設定します。

void CMFCApplication1Dlg::OnBnClickedOk()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	//CDialogEx::OnOK();
	thread_id = 0;
	count = 0;
	flag_end = false;
	for (int i = 0; i < 100; i++){
		AfxBeginThread(MyThread, this);
	}
}

ボタン1を押すとcountとthread_idを確認できるようにします。

void CMFCApplication1Dlg::OnBnClickedButton1()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	CString ss;
	ss.Format(_T("thread_id=%d,count=%d"), thread_id, count);
	m_label.SetWindowTextW(ss);
}

実行すると

thread_id=100,count=1000

となり、countに競合がおきずに正しく動作することがわかります。

著者:安井 真人(やすい まさと)