📌

C++Builderで排他制御を試してみた

2023/05/21に公開

排他制御を試してみる

複数のアプリケーションのデータをDatasnapサーバに集めてAWSにアップロードしようとした。AWSとはWebSocketのコネクションを張る。しかしDatasnapはクライアントごとにスレッドが作成されるため、コネクションオブジェクトの使用には排他制御が必要ということに思い至った。
https://docwiki.embarcadero.com/RADStudio/Alexandria/ja/サーバー_クラス_インスタンスのライフサイクル
リアルタイム性は不要のため、多少コストがかかっても楽に実装できるものはないか、簡単なforループのサンプルコードで検証してみた。

Datasnapサーバの前提

Datasnapサーバは、各クランアントからの依頼をキューにためて順次処理する。今回はstd::listを使う。

1.std::shared_prt + atomic変数を使う → 失敗

std::shard_prtはスレッド間でのデータ共有が可能で、Read,Writeの同時実行にも対応してくれる優れたclassである。排他制御でのロック/リリースは面倒なので、できれば済ませたい。
結果、うまくいかなかった。

class TForm1 : public TForm
{
__published:	// IDE で管理されるコンポーネント
private:	// ユーザー宣言
public:		// ユーザー宣言
	__fastcall TForm1(TComponent* Owner);
	// 適当な構造体を定義
	struct Obj{
		int integer;
	};
	std::list<std::shared_ptr<Obj>> list; 
	std::atomic<int> counter; // 1で初期化する
};

単純にcounterの値を代入した構造体内をひたすらstd::listに追加していくコードである。
並列プログラミングのTTaskを使って同じコードを実行させてその結果を見た。
予想通りなら1~2000までの数字が並び、最終的に2000個のデータが確認できるはず。

void __fastcall TForm1::Button4Click(TObject *Sender)
{
	_di_ITask task1 = TTask::Create([&](){
		for ( int i = 0; i < 1000; i++) {
			std::shared_ptr<Obj> obj(new Obj);
			obj->integer = counter;
			list.push_back(obj);
			counter++;
		}
	});
	_di_ITask task2 = TTask::Create([&](){
		for ( int i = 0; i < 1000; i++) {
			std::shared_ptr<Obj> obj(new Obj);
			obj->integer = counter;
			list.push_back(obj);
			counter++;
		}
	});

	task1->Start();
	task2->Start();
}

実行結果
データの並び:並び以前の問題
データの個数:1946個(想定では2000個)

...
......
.........
list.integer = 405
list.integer = 406
list.integer = 408
list.integer = 408
list.integer = 410
list.integer = 412
list.integer = 414
list.integer = 415
list.integer = 417
list.integer = 1746415413 // ????
list.integer = 1
list.integer = 2
list.integer = 3
list.integer = 4
list.integer = 5
list.size = 1946 // 全要素が1946個....

詳しく検証したかったのだが、時間が足りないので実装方法そのものを変更する。

2.排他制御(TMultiReadExclusiveWriteSynchronizer)を使う → 成功

排他制御用の仕組みはいくつか存在するが、TMultiReadExclusiveWriteSynchronizerを使用する。今回はclassのメリットを生かせないが、ロック手段がReadロックとWriteロックに分かれており、特にRead回数が多いアプリケーションでは効率的に動作する。
https://docwiki.embarcadero.com/Libraries/Alexandria/ja/System.SysUtils.TMultiReadExclusiveWriteSynchronizer
こうなるとshared_ptrもatomicも必要ないため、宣言を戻す。

class TForm1 : public TForm
{
__published:	// IDE で管理されるコンポーネント
private:	// ユーザー宣言
public:		// ユーザー宣言
	__fastcall TForm1(TComponent* Owner);
	// 適当な構造体を定義
	struct Obj{
		int integer;
	};
	std::list<Obj> list; 
	int counter; // 1で初期化する
	TMultiReadExclusiveWriteSynchronizer *ReadWriteLock; // コンストラクタでnew
};
void __fastcall TForm1::Button1Click(TObject *Sender)
{
	_di_ITask task1 = TTask::Create([&](){
		for ( int i = 0; i < 1000; i++) {
			struct Obj obj;
			ReadWriteLock->BeginWrite();
			obj.integer = counter;
			list.push_back(obj);
			counter++;
			ReadWriteLock->EndWrite();
		}
	});
	_di_ITask task2 = TTask::Create([&](){
		for ( int i = 0; i < 1000; i++) {
			struct Obj obj;
			ReadWriteLock->BeginWrite();
			obj.integer = counter;
			list.push_back(obj);
			counter++;
			ReadWriteLock->EndWrite();
		}
	});

	task1->Start();
	task2->Start();
}

実行結果
データの並び:正常
データの個数:2000個

...
......
.........
list.integer = 1989
list.integer = 1990
list.integer = 1991
list.integer = 1992
list.integer = 1993
list.integer = 1994
list.integer = 1995
list.integer = 1996
list.integer = 1997
list.integer = 1998
list.integer = 1999
list.integer = 2000
list.size = 2000

正常に動作した。排他制御の未熟さを痛感する。

Discussion