[C++][Windows][mutex]順番を保障したlockクラスを作る５番外編 Intelの実装queuing_mutexを試す

# この記事の対象読者
　私は「順番を保ったロックオブジェクト」を作ることにしました。その奮闘記に興味のある方向けの記事です。
　また、今回は**Intel® oneAPI Base Toolkit**を使っています。そちらに興味のある方もどうぞ。

# 本文

## 順番を保障したロックオブジェクトとは
```.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で検索してもそういうのは見つかりませんでした。
　1回から４回までの実験を紹介します。

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

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

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

https://qiita.com/GoldSmith/items/6a76e412b6d9ec2e4b32

## この記事で使うソースコードへのリンク

:::note info
**注意**
　このコードは**Intel® oneAPI Base Toolkit**をインストールしていないと、ビルド出来ません。インストール例は後程紹介します。
:::

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

![TestOrdered3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3813628/213bae5a-ce19-f139-f3c9-e6bd88f7c027.png)

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

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



### Intel oneAPI ライブラリの設定
　インストールされていない方で、試したい方は、サイト[Intel oneAPI Base Toolkit](https://www.intel.com/content/www/us/en/developer/tools/oneapi/toolkits.html#base-kit "Intel oneAPI Base Toolkit")からダウンロードしてインストールを行う事が出来ます。試しに何かビルドしてみて、ビルド手順の確認をするのもいいかもしれません。Visual Studio 2022（以下VS）からでもビルド可能です。oneAPIライブラリだけを使い、ビルドツールはMSのをそのまま使う事も出来ます。
　私は、oneAPIライブラリだけを使い、ビルドツールはMSのを利用したので、その方法を紹介します。
　ソリューションエクスプローラーの対象のプロジェクトのプロパティで、「**Intel®  Libraries for oneAPI**」という項目が新しく出来ているはずなので、「**Use oneTBB**」の項目を**はい**に設定してください。

![IntelOneAPIProperty.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3813628/cd0dd1dd-47ba-5c7f-6b89-bb362de0d85c.png)

## oneapi::tbb::queuing_mutexとは
　Intel oneAPIのmutexの一つであり、フェアなミューテックスとの事です。ネーミングがそのまま言い当ていて、「ああ、これはいい。」と思いました。

## サンプルコードの解説
　６スレッド、それぞれ３回`doWorkInOrder`関数を実行します。想定通りなら、まんべんなく、queuing_mutexでスレッドが選択されるはずです。
### TestOrdered3.cpp
```TestOrdered3.cpp
#include <Windows.h>
#include <memory>
#include <iostream>
#include <sstream>
#include <atomic>
#include <thread>
#include <iomanip>
#include <crtdbg.h>
#include "../CommonLib/defSTRINGIZE.h"
#include "../CommonLib/OrderedCout.h"
#include <oneapi/tbb/queuing_mutex.h>
#include <conio.h>
#pragma comment(lib,  "../CommonLib/" STRINGIZE($CONFIGURATION) "/CommonLib-" STRINGIZE($CONFIGURATION) ".lib")
#pragma once

using namespace std;

oneapi::tbb::queuing_mutex  mtx; // 1

static constexpr DWORD SETUP_TIME = 500; // 2
static constexpr DWORD NUM_THREADS = 6;
static constexpr DWORD NUM_TIMES = 3;
static constexpr DWORD NUM_PARAMS_UNITS = 0x1000;

HANDLE hEvStart_; // 3

atomic<unsigned> total(1);// 4

struct params{ // 5
	OrderedCOut* pOrderCout;
	size_t num;
	MemoryLoan<params>* pML;
	int times{};
};

void doWorkInOrder(params* ppms){
	oneapi::tbb::queuing_mutex::scoped_lock lk; // 6
	lk.acquire(mtx); // 7

	// ここで作業を行う
	ppms->pOrderCout->Push("thread" + to_string(ppms->num) + " "
		+ to_string(ppms->times) + " " + "times" + "       "
		+ "total pass count " + to_string(total) + "\n"); // 8

	++total;
}

unsigned __stdcall ThreadFunc(void* param){ // 9
	unique_ptr<params, void (*)(params*)> ppms = { reinterpret_cast< params* >(param)
		,[](params* p){p->pML->Return(p); } };


	WaitForSingleObject(hEvStart_, INFINITE); // 10
	for ( int i = 1; i <= NUM_TIMES; ++i ){
		ppms->times = i;
		doWorkInOrder(ppms.get());
	}
	return 0;
}

int main(){
	{
		OrderedCOut OrCout; // 10

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

		OrCout.Trigger(true); // 12

		unique_ptr<params[]> paramsArr = make_unique<params[]>(NUM_PARAMS_UNITS);
		MemoryLoan mlParams(paramsArr.get(), NUM_PARAMS_UNITS); // 13

		unique_ptr<HANDLE[]> threads = make_unique<HANDLE[]>(NUM_THREADS);
		for ( size_t i = 0; i < NUM_THREADS; ++i ){
			params* pPrms = &((*mlParams.Lend()) = {&OrCout,i,&mlParams,0 });
			if ( !(threads[ i ] = ( HANDLE ) _beginthreadex(
				nullptr, 0, &ThreadFunc, pPrms, 0, nullptr)) ){
				ENOut(errno);
			}
		}

		Sleep(SETUP_TIME); // 14
		SetEvent(hEvStart_); // 15

		for ( size_t i = 0; i < NUM_THREADS; ++i ){ // 16
			DWORD dw;
			if ( !((dw = ::WaitForSingleObject(threads[ i ], INFINITE)) == WAIT_OBJECT_0) ){
				ENOut(::GetLastError());
			}
		}

		for ( size_t i = 0; i < NUM_THREADS; ++i ){ // 17
			CloseHandle(threads[ i ]);
		}

		OrCout.StopTimer();// 18
		stringstream ss;
		ss << "Main:total elapsed time: "
			<< fixed << setprecision(3)
			<< OrCout.TotalTime()
			<< " msec" << endl;
		OrCout.Push(ss.str()); // 19
		OrCout.MessageFlush(); // 20

	}
	_CrtDumpMemoryLeaks();
	( void ) _getch();
	return 0;
}
```
#### 1\.oneapi::tbb::queuing_mutex  mtx
　グローバルで作成します。

#### 2\.
　各定数を定義しています。

#### 3\. HANDLE hEvStart_
　各スレッドが一斉にスタートする為のイベントオブジェクトハンドルです。

#### 4\. atomic<unsigned> total(1)
　トータルの通過カウントを取ります。

#### 5\. struct params
　スレッドに渡すparams構造体を定義しています。

#### 6\. 7\. oneapi::tbb::queuing_mutex::scoped_lock lk
 スレッドプロシージャから実行される関数です。ロックオブジェクトを作ります。この実装方法に気付けませんでした。また、作るときは、この実装方法を参考にしようと思います。

#### 8\. 
　作業内容です。コマンドプロンプトに現在のスレッドのメッセージを表示するようにしています。

#### 9\. 10\. unsigned __stdcall ThreadFunc(void* param)
　スレッドプロシージャです。`	WaitForSingleObject(hEvStart_, INFINITE)`で全てのスレッドがそろうのを待ってからスタートします。

#### 11\. OrderedCOut OrCout
　コマンドプロンプトの表示のギャップタイムを計測し表示するオブジェクトです。ギャップタイムには計測するコードを実行する時間も含まれています。

#### 12\. OrCout.Trigger(true)
　最初の表示をトリガーとして計測を始めます。

#### 13\. MemoryLoan mlParams(paramsArr.get(), NUM_PARAMS_UNITS)
　スレッドにパラメーターを渡す為の、構造体paramsのメモリープールです。

#### 14\. Sleep(SETUP_TIME)
　セットアップタイムを設けています。

#### 15\. SetEvent(hEvStart_)
　ここで、各スレッドを一斉にスタートさせます。

#### 16\. for ( size_t i = 0; i < NUM_THREADS; ++i )
　スレッドを終了待ちする為のループです。stdライブラリのjoinと似たようなものです。

#### 17\. for ( size_t i = 0; i < NUM_THREADS; ++i )
　スレッドのハンドルを閉じるループです。

#### 18\.~20\.
　コマンドプロンプトに表示するメッセージをフラッシュして、トータル経過時間の表示をします。

## 実行結果
```
thread0 1 times       total pass count 1        0.000 msec
thread4 1 times       total pass count 2        0.010 msec
thread3 1 times       total pass count 3        0.574 msec
thread5 1 times       total pass count 4        0.448 msec
thread2 1 times       total pass count 5        0.413 msec
thread1 1 times       total pass count 6        0.487 msec
thread0 2 times       total pass count 7        0.525 msec
thread4 2 times       total pass count 8        0.473 msec
thread3 2 times       total pass count 9        0.514 msec
thread5 2 times       total pass count 10       0.489 msec
thread2 2 times       total pass count 11       0.422 msec
thread1 2 times       total pass count 12       0.401 msec
thread0 3 times       total pass count 13       0.431 msec
thread4 3 times       total pass count 14       0.422 msec
thread3 3 times       total pass count 15       0.403 msec
thread5 3 times       total pass count 16       0.415 msec
thread2 3 times       total pass count 17       0.415 msec
thread1 3 times       total pass count 18       0.748 msec
Main:total elapsed time: 7.598 msec
```
　thread0からthread5まで、まんべんなくスレッドが通過しています。

## まとめ
* **Intel® oneAPI Tool kit**の、**queuing_mutex**を試してみた
* VSからでも使用可能だった
* 期待通りの動作をした

# 本文終了

# 終わりに
　「[C++][Windows][mutex]順番を保障したlockクラスを作る５番外編 Intelの実装queuing_mutexを試す」の解説は以上となります。この記事が皆様の閃きや発想のきっかけになりましたら幸いです。
　また、ご意見、ご感想、ご質問など、お待ちしております。