Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion include/kf/ThreadPool.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ namespace kf
}

private:
Thread m_threads[kMaxCount];
Thread m_threads[kMaxCount]{};
int m_count;
};
}
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ wdk_add_driver(kf-test WINVER NTDDI_WIN10 STL
AutoSpinLockTest.cpp
EResourceSharedLockTest.cpp
RecursiveAutoSpinLockTest.cpp
EResourceTest.cpp
)

target_link_libraries(kf-test kf::kf kmtest::kmtest)
Expand Down
224 changes: 224 additions & 0 deletions test/EResourceTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
#include "pch.h"
#include <kf/EResource.h>
#include <kf/ThreadPool.h>

namespace
{
constexpr int kOneMillisecond = 10'000; // 1 ms in 100-nanosecond intervals

void delay()
{
LARGE_INTEGER interval;
interval.QuadPart = -kOneMillisecond;
KeDelayExecutionThread(KernelMode, FALSE, &interval);
}
}

SCENARIO("kf::EResource")
{
GIVEN("An EResource object")
{
kf::EResource resource;

THEN("The resource is initialized and not acquired immediately")
{
REQUIRE(resource.isAcquiredExclusive() == false);
REQUIRE(resource.isAcquiredShared() == 0);
}
}

GIVEN("The EResource acquired exclusive")
{
kf::EResource resource;
bool acquired = resource.acquireExclusive();

WHEN("The resource is acquired exclusively")
{
//The system considers exclusive access to be a subset of shared access.
//Therefore, a thread that has exclusive access to a resource also has shared access to the resource.
THEN("The exclusive and shared locks are acquired")
{
REQUIRE(acquired == true);
REQUIRE(resource.isAcquiredExclusive() == true);
REQUIRE(resource.isAcquiredShared() == 1);
}

WHEN("The exclusive lock is released")
{
resource.release();

THEN("The resource is no longer locked")
{
REQUIRE(resource.isAcquiredExclusive() == false);
REQUIRE(resource.isAcquiredShared() == 0);
}
}
}
}

GIVEN("The EResource is acquired shared")
{
kf::EResource resource;
bool acquired = resource.acquireShared();

THEN("The only shared lock is acquired")
{
REQUIRE(acquired == true);
REQUIRE(resource.isAcquiredExclusive() == false);
REQUIRE(resource.isAcquiredShared() == 1);
}

WHEN("The shared lock is released")
{
resource.release();

THEN("The resource is no longer locked")
{
REQUIRE(resource.isAcquiredExclusive() == false);
REQUIRE(resource.isAcquiredShared() == 0);
}
}
}

GIVEN("The EResource acquired exclusive")
{
kf::EResource resource;
bool exclusiveAcquired = resource.acquireExclusive();
REQUIRE(exclusiveAcquired == true);

WHEN("The resource is converted to shared")
{
resource.convertExclusiveToShared();

THEN("The lock is converted to shared")
{
REQUIRE(resource.isAcquiredExclusive() == false);
REQUIRE(resource.isAcquiredShared() == 1);
}

WHEN("The shared lock is released")
{
resource.release();

THEN("The resource is no longer locked")
{
REQUIRE(resource.isAcquiredExclusive() == false);
REQUIRE(resource.isAcquiredShared() == 0);
}
}
}
}

GIVEN("The EResource locked using the lock function")
{
kf::EResource resource;
resource.lock();

THEN("The resource is acquired exclusively and shared")
{
REQUIRE(resource.isAcquiredExclusive() == true);
REQUIRE(resource.isAcquiredShared() == 1);
}

WHEN("The resource is unlocked")
{
resource.unlock();

THEN("The resource is no longer locked")
{
REQUIRE(resource.isAcquiredExclusive() == false);
REQUIRE(resource.isAcquiredShared() == 0);
}
}
}

GIVEN("The EResource is locked shared using the lock_shared function")
{
kf::EResource resource;
resource.lock_shared();

THEN("The resource is acquired shared")
{
REQUIRE(resource.isAcquiredExclusive() == false);
REQUIRE(resource.isAcquiredShared() == 1);
}

WHEN("The resource is unlocked shared")
{
resource.unlock_shared();

THEN("The resource is no longer locked")
{
REQUIRE(resource.isAcquiredExclusive() == false);
REQUIRE(resource.isAcquiredShared() == 0);
}
}
}

GIVEN("The EResource and multiple threads")
{
constexpr int kMaxThreadsCount = 64;
const ULONG numLogicalProcessors = KeQueryActiveProcessorCount(nullptr);
REQUIRE(numLogicalProcessors <= kMaxThreadsCount);

struct Context
{
kf::EResource* resource = nullptr;
std::array<bool, kMaxThreadsCount>* acquired = nullptr;
LONG counter = 0;
};
kf::EResource resource;
std::array<bool, kMaxThreadsCount> acquired;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check that numLogicalProcessors <= kMaxThreadsCount

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KeQueryActiveProcessorCount returns a number of processors for one processor group, which is a static set of up to 64 logical processors. So, the return value can never be greater than 64.
https://learn.microsoft.com/en-us/windows/win32/procthread/processor-groups#:~:text=Support%20for%20systems,group%2C%20Group%200.
Even if it could, the ThreadPool constructor explicitly caps the number of worker threads at 64

Context context{ &resource, &acquired };
kf::ThreadPool threadPool(numLogicalProcessors);

WHEN("Multiple threads attempt to acquire exclusive the resource")
{
threadPool.start([](void* context) {
auto res = static_cast<Context*>(context);
res->resource->acquireExclusive();
res->acquired->at(res->counter++) = true;
delay();
res->resource->release();
}, &context);

threadPool.join();

THEN("All threads can acquire and release the resource without deadlock")
{
auto size = min(kMaxThreadsCount, numLogicalProcessors);
for (size_t i = 0; i < size; i++)
{
REQUIRE(acquired.at(i) == true);
}
REQUIRE(resource.isAcquiredExclusive() == false);
REQUIRE(resource.isAcquiredShared() == 0);
}
}

WHEN("Multiple threads attempt to acquire shared the resource")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a better way to check that shared access was successfull without waiting. Create a hundred of threads that read some resource and register this event, then join them all and then check that every consumer got what it wanted

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added ThreadPool with maximum count of logical threads

{
threadPool.start([](void* context) {
auto res = static_cast<Context*>(context);
res->resource->acquireShared();
auto index = InterlockedIncrement(&res->counter) - 1;
res->acquired->at(index) = true;
delay();
res->resource->release();
}, &context);

threadPool.join();

THEN("All threads can acquire and release the resource without deadlock")
{
auto size = min(kMaxThreadsCount, numLogicalProcessors);
for (size_t i = 0; i < size; i++)
{
REQUIRE(acquired.at(i) == true);
}
REQUIRE(resource.isAcquiredExclusive() == false);
REQUIRE(resource.isAcquiredShared() == 0);
}
}
}
}
13 changes: 13 additions & 0 deletions test/pch.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ extern "C" inline int _CrtDbgReport(
KeBugCheckEx(KERNEL_SECURITY_CHECK_FAILURE, 0, 0, 0, 0);
}

inline void __ehvec_dtor(
void* ptr,
unsigned __int64 size,
unsigned __int64 count,
void(__cdecl* dtor)(void*)
)
{
UNREFERENCED_PARAMETER(ptr);
UNREFERENCED_PARAMETER(size);
UNREFERENCED_PARAMETER(count);
UNREFERENCED_PARAMETER(dtor);
}

namespace std
{
[[noreturn]] inline void __cdecl _Xinvalid_argument(_In_z_ const char* /*What*/)
Expand Down