この記事の対象読者
私は「順番を保ったロックオブジェクト」を作ることにしました。その奮闘記の続きです。今回は新たに、OrderedLockというAPCキューを使ったクラスの原理の解説です。
本文
順番を保障したロックオブジェクトとは
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で検索してもそういうのは見つかりませんでした。
前々回、前回の実験を紹介します。
デモコード
プログラムの事前説明
このプログラムは、順番を保証できない同期オブジェクトで作った、ロックする順番を保障するプログラムを作る為のテストプログラムです。Ordered Lockクラスと命名しました。
この記事で使うソースコードへのリンク
GitHubへのリンクはここです。Visual Studio 2022用に設定されたslnファイルもあります。
TestProjectをスタートアッププロジェクトに設定し、ソリューションエクスプローラーからTestOrdered.cppを選択し、プロパティの設定で全般->ビルドから除外項目をいいえに設定し、TestOrdered.cpp以外ははいに設定し、ターゲットCPUをx64に設定し、F5
を押下すると実行できます。
また、記事中でいくつかのソースファイルを使いますが、その都度そのファイルを全般->ビルドから除外 の設定を変更して、一つのファイルをビルド対象にしていただければ、ビルド可能になります。
ソースコードの中にはデバッグ用のライブラリ、また、表示のギャップ時間を計測するクラスOrCout
も含んでいます。本質ではない為、今回は説明を割愛いたします。
この記事で紹介しているソースコードは、公開した時点から変更を加えている事があります。そのため、元の記事とは異なる結果を得る場合があります。また、ソースコードを機能別にディレクトリを分ける等の、改善を行う可能性があります。
フローチャート
OrderLockクラスを使ったスレッドの流れです。これが全てと言っていいです。後は細かなルーチンを肉付けしていきました。
ソースコードの解説
フロチャートが全てで、コードも短いのでさらっと解説します。
OrderLock.cpp
#include "OrderLock.h"
OrderLock::OrderLock():
__pBucket{ new bucket[NUM_LOCK] }
,__mlBuckets(__pBucket, NUM_LOCK)
,__hEventEndThread{[](){
HANDLE h;
if( !(h = CreateEvent(NULL, TRUE, FALSE, NULL)) ){
throw std::exception("CreateEvent");
} return h; }(), CloseHandle }
, __hEventHost{ [](){
HANDLE h;
if( !(h = CreateEvent(NULL, TRUE, FALSE, NULL)) ){
throw std::exception("CreateEvent");
} return h; }(), CloseHandle }
, __pAPCCallBack{ [](ULONG_PTR Parameter){
bucket *pBucket = reinterpret_cast<bucket*>(Parameter);
SetEvent(pBucket->hEvent.get());
ResetEvent(pBucket->self->__hEventHost.get());
WaitForSingleObject(pBucket->self->__hEventHost.get(), INFINITE);
} }
, __pThreadWarkerProc{ [](LPVOID pvoid)->DWORD{
HANDLE hEvent = reinterpret_cast<HANDLE>(pvoid);
for( ;;){
DWORD dw = ::WaitForSingleObjectEx(hEvent, INFINITE, TRUE);
if( dw == WAIT_IO_COMPLETION ){
OutputDebugStringA("APC executed.\r\n");
} else if( dw == WAIT_OBJECT_0 ){
OutputDebugStringA("Event signaled.\r\n");
return 0;
} else{
std::cerr << "err" << std::endl;
return 123;
}
}
} }
{
if( !(__hThreadHost = CreateThread(
NULL
, 0
, __pThreadWarkerProc
, __hEventEndThread.get()
, 0
, NULL)) ){
throw std::exception("CreateThread");
};
}
OrderLock::~OrderLock(){
SetEvent(__hEventEndThread.get());
WaitForSingleObject(__hThreadHost, INFINITE);
delete[]__pBucket;
}
void OrderLock::Lock(){
bucket* pBucket = __mlBuckets.Lend();
pBucket->self = this;
pBucket->hThreadGest = GetCurrentThread();
ResetEvent(pBucket->hEvent.get());
QueueUserAPC(__pAPCCallBack, __hThreadHost, (ULONG_PTR)pBucket);
WaitForSingleObject(pBucket->hEvent.get(),INFINITE);
return ;
}
void OrderLock::UnLock(){
SetEvent(__hEventHost.get());
__mlBuckets.Return(__pCurrentBucket);
}
OrderLock::bucket::bucket():
hEvent{ [](){HANDLE h; if( !(h = CreateEvent(NULL,TRUE,FALSE,NULL)) ){
std::string str = debug_fnc::ENOut(GetLastError());
throw std::exception(str.c_str());} return h;}()
,CloseHandle }
{}
OrderLock.h
#include <Windows.h>
#include <iostream>
#include <memory>
#include <type_traits>
#include <exception>
#include "../CommonLib/MemoryLoan.h"
#include "../Debug_fnc/debug_fnc.h"
#pragma once
class OrderLock{
static constexpr DWORD NUM_LOCK = 0x40;
public:
OrderLock();
OrderLock(const OrderLock&) = delete;
OrderLock& operator=(const OrderLock&) = delete;
OrderLock& operator()(const OrderLock&) = delete;
OrderLock(OrderLock&&)noexcept = delete;
OrderLock& operator=(OrderLock&&)noexcept = delete;
OrderLock& operator()(OrderLock&&)noexcept = delete;
~OrderLock();
void Lock();
void UnLock();
private:
struct bucket{
bucket();
bucket(const bucket&) = delete;
bucket(bucket&&) = delete;
bucket& operator =(const bucket&) = delete;
bucket& operator =(bucket&&)noexcept = delete;
bucket& operator ()(const bucket&) = delete;
bucket& operator ()(bucket&&)noexcept = delete;
OrderLock* self{};
HANDLE hThreadGest{};
std::unique_ptr<std::remove_pointer_t< HANDLE>,decltype(CloseHandle)*> hEvent;
};
bucket* __pBucket{};
MemoryLoan<bucket> __mlBuckets;
PAPCFUNC const __pAPCCallBack;
LPTHREAD_START_ROUTINE const __pThreadWarkerProc;
std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(CloseHandle)*> __hEventHost;
std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(CloseHandle)*> __hEventEndThread;
HANDLE __hThreadHost;
bucket* __pCurrentBucket{};
};
APCコールバックと共に渡される引数は、bucket
と、いう構造体に収めています。このbucket
のメンバー変数の、hEvent
により、スレッドがAPCキューに、入る前にWaitForSingleObject
によりブロックされます。キューから取り出された時、hEvent
がシグナルにされ、ブロック解除される仕組みになっています。
まとめ
- mutexや他の同期オブジェクトで複数のスレッドを扱う場合、コードでは見えて来ないシステムの背景も考慮しなければならない事もある事が判った
- APCキューが使えるシーンならば、これを使うのも、一つの選択肢に入るほど、軽量で確実に出来る事が判った
終わりに
「[C++][Windows][mutex]順番を保障したlockクラスを作る3」の解説は以上となります。この記事が皆様の閃きや発想のきっかけになりましたら幸いです。
また、ご意見、ご感想、ご質問など、お待ちしております。