この記事の対象読者
私は「順番を保ったロックオブジェクト」を作ることにしました。その奮闘記に興味のある方向けの記事です。
また、今回はIntel® oneAPI Base Toolkitを使っています。そちらに興味のある方もどうぞ。
本文
順番を保障したロックオブジェクトとは
std::mutex mtx;
void add_value(int value){
std::lock_guard<std::mutex> lock(mtx);
// 何か作業をする。
}
上記のコードは1つのスレッドだけ通すコードです。後から来たスレッドはstd::lock_guard<std::mutex> lock(mtx)
で待たされる事になります。
この時、最初に待たされたスレッドが、次にmutexの所有権を持てそうな気しますが、実は、次にmutexを所有できるスレッドの順番は保障されていません。 Windowsでは、CriticalSection
がXPまでは、順番が保障されていましたが、現在は保障されていません。
ですから、その機能が欲しい場合、手作りするしかなさそうです。webで検索してもそういうのは見つかりませんでした。
1回から4回までの実験を紹介します。
この記事で使うソースコードへのリンク
注意
このコードはIntel® oneAPI Base Toolkitをインストールしていないと、ビルド出来ません。インストール例は後程紹介します。
GitHubへのリンクはここです。Visual Studio 2022用に設定されたslnファイルもあります。
TestProjectをスタートアッププロジェクトに設定し、ソリューションエクスプローラーからTestOrdered3.cppを選択し、プロパティの設定で全般->ビルドから除外項目をいいえに設定し、TestOrdered3.cpp以外ははいに設定し、ターゲットCPUをx64に設定し、F5
を押下すると実行できます。
また、記事中でいくつかのソースファイルを使いますが、その都度そのファイルを全般->ビルドから除外 の設定を変更して、一つのファイルをビルド対象にしていただければ、ビルド可能になります。
ソースコードの中にはデバッグ用のライブラリ、また、表示のギャップ時間を計測するクラスOrCout
も含んでいます。本質ではない為、今回は説明を割愛いたします。
この記事で紹介しているソースコードは、公開した時点から変更を加えている事があります。そのため、元の記事とは異なる結果を得る場合があります。また、ソースコードを機能別にディレクトリを分ける等の、改善を行う可能性があります。
Intel oneAPI ライブラリの設定
インストールされていない方で、試したい方は、サイトIntel oneAPI Base Toolkitからダウンロードしてインストールを行う事が出来ます。試しに何かビルドしてみて、ビルド手順の確認をするのもいいかもしれません。Visual Studio 2022(以下VS)からでもビルド可能です。oneAPIライブラリだけを使い、ビルドツールはMSのをそのまま使う事も出来ます。
私は、oneAPIライブラリだけを使い、ビルドツールはMSのを利用したので、その方法を紹介します。
ソリューションエクスプローラーの対象のプロジェクトのプロパティで、「Intel® Libraries for oneAPI」という項目が新しく出来ているはずなので、「Use oneTBB」の項目をはいに設定してください。
oneapi::tbb::queuing_mutexとは
Intel oneAPIのmutexの一つであり、フェアなミューテックスとの事です。ネーミングがそのまま言い当ていて、「ああ、これはいい。」と思いました。
サンプルコードの解説
6スレッド、それぞれ3回doWorkInOrder
関数を実行します。想定通りなら、まんべんなく、queuing_mutexでスレッドが選択されるはずです。
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 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クラスを作る5番外編 Intelの実装queuing_mutexを試す」の解説は以上となります。この記事が皆様の閃きや発想のきっかけになりましたら幸いです。
また、ご意見、ご感想、ご質問など、お待ちしております。