[C++][Windows][mutex]順番を保障したlockクラスを作る２

# この記事の対象読者
　私は「**順番を保ったロックオブジェクト**」を作ることにしました。その奮闘記を見たい人向けです。

# 本文
## 順番を保障したロックオブジェクトとは
```.cpp
std::mutex mtx;
void add_value(int value){
    std::lock_guard<std::mutex> lock(mtx);
    // 何か作業をする。
}
```
　上記のコードは１つのスレッドだけ通すコードです。後から来たスレッドは`std::lock_guard<std::mutex> lock(mtx)`で待たされる事になります。
　この時、最初に待たされたスレッドが、次にmutexの所有権を持てそうな気しますが、実は、**次にmutexを所有できるスレッドの順番は保障されていません。**　Windowsでは、`CriticalSection`がXPまでは、順番が保障されていましたが、現在は保障されていません。
　ですから、その機能が欲しい場合、手作りするしかなさそうです。webで検索してもそういうのは見つかりませんでした。

## 前回までのあらすじ
　下記のリンクの続きです。

https://qiita.com/GoldSmith/items/c5133646afd3047322d3

　std::mutexを使って実現しようとしました。肝となる部分は次の所です。
```TestOrderedLockStdMutex.cpp
void doWorkInOrder(int num, int i){ // 4
	mtx1.lock(); // 5
	mtx2.lock(); // 5
	mtx3.lock(); // 5

	// ここで作業を行う
	OrCout.Push("thread" + to_string(num) + " " + to_string(i) + "\n"); // 8

	mtx1.unlock(); // 6
	//Sleep(1); // 7
	mtx2.unlock(); // 6
	//Sleep(1); // 7
	mtx3.unlock(); // 6
}
```
これで、３スレッドまでは、順序を維持したロックが出来るはずでした。しかし、実際はそうにはなりませんでした。`Sleep(1)`を挟んで、やっと期待通りの動きをしました。
```TestOrderedLockStdMutex.cpp
	for( int i = 0; i < 5; ++i ){
		doWorkInOrder(num, i);
	}
```
　このようにループさせているのですが、mutexで待たされるスレッドはタイムスライスで別のコードを実行している為、mutexの所有権の取得のタイミングで、抽選に参加出来ていないのではないかと考えました。spinの意味はここにあるでは、思っているのですが、現在stdライブラリではspinはサポートされていません。OSでもシステムコールレベルでサポートされているのは、クリティカルセクションくらいしか見当たりません。これは、別で検証したいと思っています。

## 新たな方法の模索
　今回試した方法は、APCキューが使えるのではと思い、APCキューで組み立てる事としました。
### APCキューとは

https://learn.microsoft.com/ja-jp/windows/win32/sync/asynchronous-procedure-calls

　要約すると、各スレッドが持っているキューで、`QueueUserAPC`APIでキューにコールバック関数をプッシュし、`SleepEx`、`WaitForSingleObjectEx`などの**Ex系**APIでキューにスレッドを渡し、コールバック関数を呼び出す機構です。
## デモコード
### プログラムの事前説明
　このプログラムは、順番を保証できない同期オブジェクトで作った、**ロックする順番を保障するプログラムを作る為のテストプログラム**です。**Ordered Lock**クラスと命名しました。

### この記事で使うソースコードへのリンク
[GitHubへのリンクはここです。Visual Studio 2022用に設定されたslnファイルもあります。](https://github.com/NewGoldSmith/SubProcess "https://github.com/NewGoldSmith/SubProcess")
　**TestProject**をスタートアッププロジェクトに設定し、ソリューションエクスプローラーから**TestOrdered.cpp**を選択し、プロパティの設定で**全般->ビルドから除外**項目を**いいえ**に設定し、**TestOrdered.cpp以外**は**はい**に設定し、ターゲットCPUをx64に設定し、`F5`を押下すると実行できます。

![TestOrdered1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3813628/1df5b980-226a-6976-4fa8-7f776035b3f0.png)

　また、記事中でいくつかのソースファイルを使いますが、その**都度**そのファイルを**全般->ビルドから除外** の設定を変更して、一つのファイルをビルド対象にしていただければ、ビルド可能になります。
　ソースコードの中にはデバッグ用のライブラリ、また、表示のギャップ時間を計測するクラス`OrCout`も含んでいます。本質ではない為、今回は説明を割愛いたします。

:::note info
 　この記事で紹介しているソースコードは、公開した時点から変更を加えている事があります。そのため、元の記事とは異なる結果を得る場合があります。また、ソースコードを機能別にディレクトリを分ける等の、改善を行う可能性があります。
:::

　また、このプログラムはメモリープールコンテナを使っています。

https://qiita.com/GoldSmith/items/1de197c10c05c054461a


### コードの解説
　下記にサンプルコードを記載します。その後、番号のコメントが付けられているところの、解説を順次行います。実行結果は、各々が確認してもらえればと思います。
```TestOrdered.cpp
#include <Windows.h>
#include <memory>
#include <iostream>
#include <sstream>
#include <thread>
#include <iomanip>
#include <crtdbg.h>
#include "../CommonLib/defSTRINGIZE.h"
#include "../CommonLib/OrderedCout.h"
#include "../CommonLib/OrderLock.h"
#pragma comment(lib,  "../CommonLib/" STRINGIZE($CONFIGURATION) "/CommonLib-" STRINGIZE($CONFIGURATION) ".lib")
#pragma once

using namespace std;

HANDLE hEvStart_; // 1

void doWorkInOrder(OrderLock& lock, OrderedCOut& COut, int num, int i){ // 2
	lock.Lock(); // 3

	// ここで作業を行う
	COut.Push("thread" + to_string(num) + " " + to_string(i) + "\n"); // 4

	lock.UnLock(); // 3
}

void threadFunc(OrderLock& lock, OrderedCOut& OrCOut, int num){ // 5
	WaitForSingleObject(hEvStart_, INFINITE);
	for( int i = 0; i < 5; ++i ){
		doWorkInOrder(ref(lock), ref(OrCOut), num, i);
	}
}

int main(){ // 6
	{
		OrderedCOut OrCout; // 7

		unique_ptr<remove_pointer_t<HANDLE>, decltype(CloseHandle)*> hEvStart{ [&](){
		if( !(hEvStart_ = CreateEvent(NULL, TRUE, FALSE, NULL)) ){
			throw std::exception("CreateEvent");
		} return hEvStart_; }(), CloseHandle }; // 8

		OrCout.Trigger(true); // 9

		OrderLock lock; // 10

		thread t1(threadFunc, ref(lock), ref(OrCout), 1); // 11
		thread t2(threadFunc, ref(lock), ref(OrCout), 2); // 11
		thread t3(threadFunc, ref(lock), ref(OrCout), 3); // 11

		Sleep(200); // 12
		SetEvent(hEvStart_); // 13

		t1.join(); // 14
		t2.join(); // 14
		t3.join(); // 14

		OrCout.StopTimer();// 15
		stringstream ss;
		ss << "Main:total elapsed time: "
			<< fixed << setprecision(3)
			<< OrCout.TotalTime()
			<< " msec" << endl;
		OrCout.Push(ss.str()); // 16
		OrCout.MessageFlush(); // 17
	}
	_CrtDumpMemoryLeaks();
	return 0;
}
```
#### 1\. HANDLE hEvStart_
　各スレッドを一斉にスタートさせる為のイベントハンドルを格納する変数を定義しています。
#### 2\. 3\. 4\. void doWorkInOrder(OrderLock& lock, OrderedCOut& COut, int num, int i)
```TestOrdered.cpp
void doWorkInOrder(OrderLock& lock, OrderedCOut& COut, int num, int i){ // 2
	lock.Lock(); // 3

	// ここで作業を行う
	COut.Push("thread" + to_string(num) + " " + to_string(i) + "\n"); // 4

	lock.UnLock(); // 3
}
```
　実際のワークを行う関数です。各スレッドで実行されますが、`lock.Lock()`、`lock.UnLock()`で１つのスレッドのみ通します。`COut.Push("thread" + to_string(num) + " " + to_string(i) + "\n")`はワークの内容で、コンソールに文字列を出力し、前の表示との時間差も表示します。

#### 5\. 12\. 13\. void threadFunc(OrderLock& lock, OrderedCOut& OrCOut, int num)
```TestOrdered.cpp
void threadFunc(OrderLock& lock, OrderedCOut& OrCOut, int num){ // 5
	WaitForSingleObject(hEvStart_, INFINITE);
	for( int i = 0; i < 5; ++i ){
		doWorkInOrder(ref(lock), ref(OrCOut), num, i);
	}
}
```
　スレッドプロシージャです。各スレッドがこのスレッドプロシージャで動きます。`WaitForSingleObject(hEvStart_, INFINITE)`は、全てのスレッドが準備出来るまで待ちます。
```TestOrdered.cpp
		Sleep(200); // 12
		SetEvent(hEvStart_); // 13
```
　上記の所で、イベントがシグナルになると、一斉にスタートします。

#### 6\. int main()
　main関数です。

#### 7\. OrderedCOut OrCout
　コンソール出力に、前の表示からの経過時間を計測して表示するオブジェクトを作成します。

#### 8\.
```TestOrdered.cpp
		unique_ptr<remove_pointer_t<HANDLE>, decltype(CloseHandle)*> hEvStart{ [&](){
		if( !(hEvStart_ = CreateEvent(NULL, TRUE, FALSE, NULL)) ){
			throw std::exception("CreateEvent");
		} return hEvStart_; }(), CloseHandle }; // 8
```
　一斉にスタートする為のイベントオブジェクトを定義しています。

#### 9\. OrCout.Trigger(true)
　最初の表示から、経過時間を計測するように指示を出します。

#### 10\. OrderLock lock
　ロックオブジェクトを定義しています。

#### 11\. 
```TestOrdered.cpp
		thread t1(threadFunc, ref(lock), ref(OrCout), 1); // 11
		thread t2(threadFunc, ref(lock), ref(OrCout), 2); // 11
		thread t3(threadFunc, ref(lock), ref(OrCout), 3); // 11
```
 　３つスレッドを作成しています。これが順番に実行されることを想定しています。
#### 14\.
```TestOrdered.cpp
		t1.join(); // 14
		t2.join(); // 14
		t3.join(); // 14
```
　スレッドの終了処理をしています。

#### 15\. 16\. 17\.
```TestOrdered.cpp
		OrCout.StopTimer();// 15
		stringstream ss;
		ss << "Main:total elapsed time: "
			<< fixed << setprecision(3)
			<< OrCout.TotalTime()
			<< " msec" << endl;
		OrCout.Push(ss.str()); // 16
		OrCout.MessageFlush(); // 17
```
　15\.で時間計測をストップさせます。16\.で、表示し、17\.でバッファをフラッシュしています。

### 以上、ソース解説終了

### 実行の結果
```コンソール.txt
thread2 0                                       0.000 msec
thread1 0                                       0.016 msec
thread3 0                                       0.694 msec
thread2 1                                       0.590 msec
thread1 1                                       0.577 msec
thread3 1                                       0.572 msec
thread2 2                                       0.534 msec
thread1 2                                       0.483 msec
thread3 2                                       0.504 msec
thread2 3                                       0.493 msec
thread1 3                                       0.495 msec
thread3 3                                       0.477 msec
thread2 4                                       0.466 msec
thread1 4                                       0.462 msec
thread3 4                                       0.465 msec
Main:total elapsed time: 6.833 msec
```
　想定通りの結果となりました。トータル経過時間も7msec弱なので、まあまあいいのではないでしょうか。
# まとめ
* 順番を保障したlockクラスを作ることにした
* std::mutexを使ったものは、上手くいかなかった
* そこで、APCキューを使ったものを作った
* 論理通り動作する事が判った
# 次回予定
　OrderLockのソースコード解説の記事を公開したいと考えています。
# 終わりに
　「[C++][Windows][mutex]順番を保障したlockクラスを作る２」の解説は以上となります。この記事が皆様の閃きや発想のきっかけになりましたら幸いです。
　また、ご意見、ご感想、ご質問など、お待ちしております。