こんにちは、私のテスト コードから予想される偽の共有が発生しないという問題が発生しています。
複数のスレッドを均一に管理するプロセス固有のスレッド マネージャーを作成しようとしています。
独自のスレッド マネージャ クラスはスレッド プールではなく、指定されたスレッドにタスク関数を割り当てて動作し、タスク関数の戻り値を取得することができます。また、スレッドマネージャはタスクのサイズ(計算量)を気にしません。
スレッド マネージャは、スレッド (メイン スレッド) によって計算部分を処理するために使用され、非常に頻繁に使用されます。この理由は、私のプロセスにはゲーム ループ デザイン パターンがあり、ゲーム ループを 120 FPS 以上にしたいためです。つまり、1 つのゲーム ループを 8.3 ミリ秒未満で実行する必要があります。スレッド (メイン スレッド) は、1 つのゲーム ループ内でこのタスクの割り当てを何度も行う可能性があるため、コンテキスト切り替えのコストを削減/排除することが私の主な関心事でした。私の結論は、スレッドマネージャーのスレッドをスピンロックさせることでした。
つまり、ゲーム ループは、次の 2 つの手順を何度も繰り返します。
- メイン ループはタスクをスレッド マネージャーに割り当てます。
- スレッド マネージャによるタスクの結果を待ちます。
以下は私のテストコードです。
ThreadManager.h
namespace YSLibrary
{
class CThreadManager final
{
private:
static long long s_llLock;
static unsigned long long s_ullThreadCount;
static void** s_ppThreads;
static unsigned long* s_pThreadIDs;
static long long* s_pThreadQuits;
static long long* s_pTaskLocks;
static unsigned long long (**s_ppTasks)();
static unsigned long long* s_pTaskResults;
CThreadManager(){}
~CThreadManager(){}
__forceinline static void Lock()
{
while (true)
{
if (InterlockedCompareExchange64(&s_llLock, 1LL, 0LL) == 0LL)
{
return;
}
Sleep(0UL);
}
}
__forceinline static void Unlock()
{
InterlockedExchange64(&s_llLock, 0LL);
}
static unsigned long __stdcall Thread(void* const _pParameter)
{
const unsigned long long ullThreadIndex = reinterpret_cast<const unsigned long long>(_pParameter);
while (true)
{
if (InterlockedCompareExchange64(&s_pThreadQuits[ullThreadIndex], 0LL, 1LL) == 1LL)
{
return 1UL;
}
if (InterlockedCompareExchange64(&s_pTaskLocks[ullThreadIndex], 1LL, 0LL) == 0LL)
{
if (s_ppTasks[ullThreadIndex] != nullptr)
{
s_pTaskResults[ullThreadIndex] = s_ppTasks[ullThreadIndex]();
s_ppTasks[ullThreadIndex] = nullptr;
}
InterlockedExchange64(&s_pTaskLocks[ullThreadIndex], 0LL);
}
}
}
public:
enum class EResult : unsigned long long
{
None = 0ULL,
Success = 1ULL,
Fail_ArgumentNull = 2ULL,
Fail_ArgumentInvalid = 3ULL,
Fail_Locked = 4ULL,
Fail_ThreadCountNotZero = 5ULL,
Fail_ThreadCountZero = 6ULL,
Fail_ThreadsNotNull = 7ULL,
Fail_ThreadsNull = 8ULL,
Fail_ThreadIDsNotNull = 9ULL,
Fail_ThreadIDsNull = 10ULL,
Fail_ThreadQuitsNotNull = 11ULL,
Fail_ThreadQuitsNull = 12ULL,
Fail_TaskLocksNotNull = 13ULL,
Fail_TaskLocksNull = 14ULL,
Fail_TasksNotNull = 15ULL,
Fail_TasksNull = 16ULL,
Fail_TaskResultsNotNull = 17ULL,
Fail_TaskResultsNull = 18ULL,
Fail_CreateThread = 19ULL
};
__forceinline static EResult Initialize(const unsigned long long _ullThreadCount)
{
if (_ullThreadCount == 0ULL)
{
return EResult::Fail_ArgumentNull;
}
Lock();
if (s_ullThreadCount != 0ULL)
{
Unlock();
return EResult::Fail_ThreadCountNotZero;
}
if (s_ppThreads != nullptr)
{
Unlock();
return EResult::Fail_ThreadsNotNull;
}
if (s_pThreadIDs != nullptr)
{
Unlock();
return EResult::Fail_ThreadIDsNotNull;
}
if (s_pThreadQuits != nullptr)
{
Unlock();
return EResult::Fail_ThreadQuitsNotNull;
}
if (s_pTaskLocks != nullptr)
{
Unlock();
return EResult::Fail_TaskLocksNotNull;
}
if (s_ppTasks != nullptr)
{
Unlock();
return EResult::Fail_TasksNotNull;
}
if (s_pTaskResults != nullptr)
{
Unlock();
return EResult::Fail_TaskResultsNotNull;
}
s_ullThreadCount = _ullThreadCount;
s_ppThreads = new void*[s_ullThreadCount]{};
s_pThreadIDs = new unsigned long[s_ullThreadCount]{};
s_pThreadQuits = new long long[s_ullThreadCount]{};
s_pTaskLocks = new long long[s_ullThreadCount]{};
s_ppTasks = new (unsigned long long (*[s_ullThreadCount])()){};
s_pTaskResults = new unsigned long long[s_ullThreadCount]{};
for (unsigned long long i = 0ULL; i < s_ullThreadCount; ++i)
{
s_ppThreads[i] = CreateThread(nullptr, 0ULL, &Thread, reinterpret_cast<void*>(i), 0UL, &s_pThreadIDs[i]);
if (s_ppThreads[i] == nullptr)
{
// Rollback
for (unsigned long long j = 0ULL; j < i; ++j)
{
InterlockedExchange64(&s_pThreadQuits[i], 1LL);
}
unsigned long ulExitCode = 0UL;
for (unsigned long long j = 0ULL; j < i; ++j)
{
while (true)
{
GetExitCodeThread(s_ppThreads[j], &ulExitCode);
if (ulExitCode != static_cast<unsigned long>(STILL_ACTIVE))
{
CloseHandle(s_ppThreads[j]);
s_ppThreads[j] = nullptr;
break;
}
Sleep(0UL);
}
}
delete[] s_pTaskResults;
s_pTaskResults = nullptr;
delete[] s_ppTasks;
s_ppTasks = nullptr;
delete[] s_pTaskLocks;
s_pTaskLocks = nullptr;
delete[] s_pThreadQuits;
s_pThreadQuits = nullptr;
delete[] s_pThreadIDs;
s_pThreadIDs = nullptr;
delete[] s_ppThreads;
s_ppThreads = nullptr;
s_ullThreadCount = 0ULL;
Unlock();
return EResult::Fail_CreateThread;
}
}
Unlock();
return EResult::Success;
}
__forceinline static EResult Terminate()
{
Lock();
if (s_ullThreadCount == 0ULL)
{
Unlock();
return EResult::Fail_ThreadCountZero;
}
if (s_ppThreads == nullptr)
{
Unlock();
return EResult::Fail_ThreadsNull;
}
if (s_pThreadIDs == nullptr)
{
Unlock();
return EResult::Fail_ThreadIDsNull;
}
if (s_pThreadQuits == nullptr)
{
Unlock();
return EResult::Fail_ThreadQuitsNull;
}
if (s_pTaskLocks == nullptr)
{
Unlock();
return EResult::Fail_TaskLocksNull;
}
if (s_ppTasks == nullptr)
{
Unlock();
return EResult::Fail_TasksNull;
}
if (s_pTaskResults == nullptr)
{
Unlock();
return EResult::Fail_TaskResultsNull;
}
for (unsigned long long i = 0ULL; i < s_ullThreadCount; ++i)
{
InterlockedExchange64(&s_pThreadQuits[i], 1LL);
}
unsigned long ulExitCode = 0UL;
for (unsigned long long i = 0ULL; i < s_ullThreadCount; ++i)
{
while (true)
{
GetExitCodeThread(s_ppThreads[i], &ulExitCode);
if (ulExitCode != static_cast<unsigned long>(STILL_ACTIVE))
{
CloseHandle(s_ppThreads[i]);
s_ppThreads[i] = nullptr;
break;
}
Sleep(0UL);
}
}
delete[] s_pTaskResults;
s_pTaskResults = nullptr;
delete[] s_ppTasks;
s_ppTasks = nullptr;
delete[] s_pTaskLocks;
s_pTaskLocks = nullptr;
delete[] s_pThreadQuits;
s_pThreadQuits = nullptr;
delete[] s_pThreadIDs;
s_pThreadIDs = nullptr;
delete[] s_ppThreads;
s_ppThreads = nullptr;
s_ullThreadCount = 0ULL;
Unlock();
return EResult::Success;
}
__forceinline static EResult Execute(const unsigned long long _ullThreadIndex, unsigned long long (*_pFunction)())
{
if (_pFunction == nullptr)
{
return EResult::Fail_ArgumentNull;
}
Lock();
if (s_ullThreadCount == 0ULL)
{
Unlock();
return EResult::Fail_ThreadCountZero;
}
if (s_ppThreads == nullptr)
{
Unlock();
return EResult::Fail_ThreadsNull;
}
if (s_pThreadIDs == nullptr)
{
Unlock();
return EResult::Fail_ThreadIDsNull;
}
if (s_pThreadQuits == nullptr)
{
Unlock();
return EResult::Fail_ThreadQuitsNull;
}
if (s_pTaskLocks == nullptr)
{
Unlock();
return EResult::Fail_TaskLocksNull;
}
if (s_ppTasks == nullptr)
{
Unlock();
return EResult::Fail_TasksNull;
}
if (s_pTaskResults == nullptr)
{
Unlock();
return EResult::Fail_TaskResultsNull;
}
if (_ullThreadIndex >= s_ullThreadCount)
{
Unlock();
return EResult::Fail_ArgumentInvalid;
}
while (true)
{
if (InterlockedCompareExchange64(&s_pTaskLocks[_ullThreadIndex], 1LL, 0LL) == 0LL)
{
s_ppTasks[_ullThreadIndex] = _pFunction;
InterlockedExchange64(&s_pTaskLocks[_ullThreadIndex], 0LL);
Unlock();
return EResult::Success;
}
Sleep(0UL);
}
}
__forceinline static EResult WaitForResult(const unsigned long long _ullThreadIndex, unsigned long long* const _pFunctionResult)
{
if (_pFunctionResult == nullptr)
{
return EResult::Fail_ArgumentNull;
}
Lock();
if (s_ullThreadCount == 0ULL)
{
Unlock();
return EResult::Fail_ThreadCountZero;
}
if (s_ppThreads == nullptr)
{
Unlock();
return EResult::Fail_ThreadsNull;
}
if (s_pThreadIDs == nullptr)
{
Unlock();
return EResult::Fail_ThreadIDsNull;
}
if (s_pThreadQuits == nullptr)
{
Unlock();
return EResult::Fail_ThreadQuitsNull;
}
if (s_pTaskLocks == nullptr)
{
Unlock();
return EResult::Fail_TaskLocksNull;
}
if (s_ppTasks == nullptr)
{
Unlock();
return EResult::Fail_TasksNull;
}
if (s_pTaskResults == nullptr)
{
Unlock();
return EResult::Fail_TaskResultsNull;
}
if (_ullThreadIndex >= s_ullThreadCount)
{
Unlock();
return EResult::Fail_ArgumentInvalid;
}
while (true)
{
if (InterlockedCompareExchange64(&s_pTaskLocks[_ullThreadIndex], 1LL, 0LL) == 0LL)
{
if (s_ppTasks[_ullThreadIndex] == nullptr)
{
(*_pFunctionResult) = s_pTaskResults[_ullThreadIndex];
InterlockedExchange64(&s_pTaskLocks[_ullThreadIndex], 0LL);
Unlock();
return EResult::Success;
}
InterlockedExchange64(&s_pTaskLocks[_ullThreadIndex], 0LL);
}
Sleep(0UL);
}
}
};
}
main.cpp
#include <iostream>
#include <Windows.h>
#include "ThreadManager.h"
long long YSLibrary::CThreadManager::s_llLock = 0LL;
unsigned long long YSLibrary::CThreadManager::s_ullThreadCount = 0ULL;
void** YSLibrary::CThreadManager::s_ppThreads = nullptr;
unsigned long* YSLibrary::CThreadManager::s_pThreadIDs = nullptr;
long long* YSLibrary::CThreadManager::s_pThreadQuits = nullptr;
long long* YSLibrary::CThreadManager::s_pTaskLocks = nullptr;
unsigned long long (**YSLibrary::CThreadManager::s_ppTasks)() = nullptr;
unsigned long long* YSLibrary::CThreadManager::s_pTaskResults = nullptr;
unsigned long long g_pResults[10]{};
struct SData
{
unsigned long long ullData[8];
};
SData g_stData{};
SData g_stData0{};
SData g_stData1{};
SData g_stData2{};
SData g_stData3{};
SData g_stData4{};
SData g_stData5{};
SData g_stData6{};
unsigned long long Function()
{
for (unsigned long long i = 0ULL; i < 70000000ULL; ++i)
{
g_stData.ullData[0] = static_cast<unsigned long long>(rand());
}
return 1ULL;
}
unsigned long long Function0()
{
for (unsigned long long i = 0ULL; i < 10000000ULL; ++i)
{
g_stData0.ullData[0] = static_cast<unsigned long long>(rand());
}
return 1ULL;
}
unsigned long long Function1()
{
for (unsigned long long i = 0ULL; i < 10000000ULL; ++i)
{
g_stData1.ullData[0] = static_cast<unsigned long long>(rand());
}
return 1ULL;
}
unsigned long long Function2()
{
for (unsigned long long i = 0ULL; i < 10000000ULL; ++i)
{
g_stData2.ullData[0] = static_cast<unsigned long long>(rand());
}
return 1ULL;
}
unsigned long long Function3()
{
for (unsigned long long i = 0ULL; i < 10000000ULL; ++i)
{
g_stData3.ullData[0] = static_cast<unsigned long long>(rand());
}
return 1ULL;
}
unsigned long long Function4()
{
for (unsigned long long i = 0ULL; i < 10000000ULL; ++i)
{
g_stData4.ullData[0] = static_cast<unsigned long long>(rand());
}
return 1ULL;
}
unsigned long long Function5()
{
for (unsigned long long i = 0ULL; i < 10000000ULL; ++i)
{
g_stData5.ullData[0] = static_cast<unsigned long long>(rand());
}
return 1ULL;
}
unsigned long long Function6()
{
for (unsigned long long i = 0ULL; i < 10000000ULL; ++i)
{
g_stData6.ullData[0] = static_cast<unsigned long long>(rand());
}
return 1ULL;
}
int main()
{
unsigned long long ullStartTick = 0ULL;
unsigned long long ullEndTick = 0ULL;
srand((unsigned int)time(nullptr));
ullStartTick = GetTickCount64();
Function();
ullEndTick = GetTickCount64();
std::wcout << L"[Main]" << std::endl;
std::wcout << ullEndTick - ullStartTick << std::endl;
YSLibrary::CThreadManager::EResult eResult = YSLibrary::CThreadManager::EResult::None;
eResult = YSLibrary::CThreadManager::Initialize(7ULL);
ullStartTick = GetTickCount64();
eResult = YSLibrary::CThreadManager::Execute(0ULL, &Function0);
eResult = YSLibrary::CThreadManager::Execute(1ULL, &Function1);
eResult = YSLibrary::CThreadManager::Execute(2ULL, &Function2);
eResult = YSLibrary::CThreadManager::Execute(3ULL, &Function3);
eResult = YSLibrary::CThreadManager::Execute(4ULL, &Function4);
eResult = YSLibrary::CThreadManager::Execute(5ULL, &Function5);
eResult = YSLibrary::CThreadManager::Execute(6ULL, &Function6);
eResult = YSLibrary::CThreadManager::WaitForResult(0ULL, &g_pResults[0]);
eResult = YSLibrary::CThreadManager::WaitForResult(1ULL, &g_pResults[1]);
eResult = YSLibrary::CThreadManager::WaitForResult(2ULL, &g_pResults[2]);
eResult = YSLibrary::CThreadManager::WaitForResult(3ULL, &g_pResults[3]);
eResult = YSLibrary::CThreadManager::WaitForResult(4ULL, &g_pResults[4]);
eResult = YSLibrary::CThreadManager::WaitForResult(5ULL, &g_pResults[5]);
eResult = YSLibrary::CThreadManager::WaitForResult(6ULL, &g_pResults[6]);
ullEndTick = GetTickCount64();
std::wcout << L"[Thread Manager]" << std::endl;
std::wcout << ullEndTick - ullStartTick << std::endl;
YSLibrary::CThreadManager::Terminate();
system("pause");
return 0;
}
インターロック系の関数、__forceinline、静的変数のダーティー宣言など、本当に申し訳ありません。
一方、ロック変数に「long long」を使用した理由は、「bool」型がなかったからです。「短い」で試してみたかったのですが、「短い」と「長い・長い」の時間を測ってみると、あまり差がありませんでした。むしろ、「ショート」の方が少し遅く、その理由は 64 ビット環境で 16 ビット レジスタを使用しているためだと思います。また、bool 型または short 型は、メモリ アラインメントの問題を引き起こす可能性があります。そこで「ロングロング」タイプを使用。
CThreadManager がプライベート コンストラクターを持つ理由は、"new CThreadManager()" を明示的に禁止するためです。
「reinterpret_cast」の使用は最小限に抑えられます。コストはコンパイル時間だと思っていましたが、スタックオーバーフローからランタイムコストがあるという質問を見ました。まだよくわかりません。そのため、スレッド関数が開始されたときに一度だけ使用してください。
これまでのところ、変更して偽共有現象を確認しました
SData::ullData[8] -> SData::ullData 1
また、Sleep(0) を使用すると、WaitForResult() でのスレッド タイム スライスの無駄が大幅に削減され、スレッド内の合計実行時間も削減されました。
このコードの結果は
[Main]
1828
[Thread Manager]
344
私の環境では。
しかし、SData::ullData 以外にも、s_pThreadQuits、s_pTaskLocks、s_ppTasks、s_pTaskResults など、偽の共有が必要な場所があることに気付きました。
これらの変数で偽共有が発生しないのはなぜですか?
[編集]
「偽の共有」とは、「異なるスレッドによってアクセスされるメモリアドレスが同じキャッシュラインを共有する」ということです。
- SData g_stDataN (各 FunctionN() 内)
- s_pThreadQuits、s_pTaskLocks、s_pTaskResults、s_ppTasks (Thread() 内)
2.変数もg_stDataN(私の環境では64バイト)と同じようにキャッシュにロードされます。「パディング」メソッドの結果を達成して偽共有を回避するために、SData のサイズを 64 バイトに設定しました。
ただし、s_pThreadQuits のサイズが 64 バイトに設定されておらず、パディングもされていない限り、フォールス シェアリングも必要です。
下のこの画像のように。
画像のソースは https://www.codeproject.com/Articles/85356/Avoiding-and-Identifying-False-Sharing-Among-Threaからのものです