#pragma region CPL License /* Nuclex Native Framework Copyright (C) 2002-2023 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #pragma endregion // CPL License // If the library is compiled as a DLL, this ensures symbols are exported #define NUCLEX_SUPPORT_SOURCE 1 #include "../Source/Threading/ThreadPoolTaskPool.h" #include // for std::unique_ptr #include // for std::mutex #include namespace { // ------------------------------------------------------------------------------------------- // /// Mock task used to test the task pool struct TestTask { /// Number of times a task constructor has been called public: static std::size_t ConstructorCallCount; /// Number of times a task destructor has been called public: static std::size_t DestructorCallCount; /// Initializes a new test task public: TestTask() { ++ConstructorCallCount; } /// Destroys a test task public: ~TestTask() { ++DestructorCallCount; } /// Size of the payload carried by the task public: std::size_t PayloadSize; /// Example content, never used, never accessed public: float Unused; /// Placeholder for the variable payload appended to the task public: std::uint8_t Payload[sizeof(std::uintptr_t)]; }; // ------------------------------------------------------------------------------------------- // std::size_t TestTask::ConstructorCallCount = 0; // ------------------------------------------------------------------------------------------- // std::size_t TestTask::DestructorCallCount = 0; // ------------------------------------------------------------------------------------------- // /// /// Used to avoid unit tests from interfering with each other in case they're run in parallel /// std::mutex CallCountMutex; // ------------------------------------------------------------------------------------------- // /// A pool of mock tasks typedef Nuclex::Support::Threading::ThreadPoolTaskPool< TestTask, offsetof(TestTask, Payload) > TestTaskPool; // ------------------------------------------------------------------------------------------- // } // anonymous namespace namespace Nuclex { namespace Support { namespace Threading { // ------------------------------------------------------------------------------------------- // TEST(ThreadPoolTaskPoolTest, HasDefaultConstructor) { EXPECT_NO_THROW( TestTaskPool taskPool; ); } // ------------------------------------------------------------------------------------------- // TEST(ThreadPoolTaskPoolTest, TaskConstructorAndDestructorAreCalled) { TestTaskPool taskPool; { std::lock_guard callCountScope(CallCountMutex); std::size_t previousConstructorCallCount = TestTask::ConstructorCallCount; std::size_t previousDestructorCallCount = TestTask::DestructorCallCount; TestTask *myTask = taskPool.GetNewTask(32); EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 1); EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount); taskPool.DeleteTask(myTask); EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 1); EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount + 1); } } // ------------------------------------------------------------------------------------------- // TEST(ThreadPoolTaskPoolTest, TasksCanBeRecycled) { TestTaskPool taskPool; { std::lock_guard callCountScope(CallCountMutex); std::size_t previousConstructorCallCount = TestTask::ConstructorCallCount; std::size_t previousDestructorCallCount = TestTask::DestructorCallCount; TestTask *originalTask = taskPool.GetNewTask(32); EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 1); EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount); taskPool.ReturnTask(originalTask); EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 1); EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount); TestTask *anotherTask = taskPool.GetNewTask(16); EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 1); EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount); EXPECT_EQ(anotherTask, originalTask); taskPool.DeleteTask(anotherTask); } } // ------------------------------------------------------------------------------------------- // TEST(ThreadPoolTaskPoolTest, RecycledTaskIsOnlyHandedOutWhenLargeEnough) { TestTaskPool taskPool; { std::lock_guard callCountScope(CallCountMutex); std::size_t previousConstructorCallCount = TestTask::ConstructorCallCount; std::size_t previousDestructorCallCount = TestTask::DestructorCallCount; TestTask *originalTask = taskPool.GetNewTask(16); EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 1); EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount); taskPool.ReturnTask(originalTask); EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 1); EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount); TestTask *anotherTask = taskPool.GetNewTask(32); EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 2); //EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount); // Originally, I tested like this: // EXPECT_NE(anotherTask, originalTask); // // but GetNewTask() calls free upon encountering the 16 byte payload task, // and the C++ memory allocator then can allocate the 32 byte payload task // at the exact same memory address. This caused spurious failures. // EXPECT_EQ(anotherTask->PayloadSize, 32U); taskPool.DeleteTask(anotherTask); } } // ------------------------------------------------------------------------------------------- // TEST(ThreadPoolTaskPoolTest, PoolDestructionKillsRecycledTasks) { std::lock_guard callCountScope(CallCountMutex); std::size_t previousConstructorCallCount = TestTask::ConstructorCallCount; std::size_t previousDestructorCallCount = TestTask::DestructorCallCount; { TestTaskPool taskPool; TestTask *myTask = taskPool.GetNewTask(32); EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 1); EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount); taskPool.ReturnTask(myTask); EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 1); EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount); } EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 1); EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount + 1); } // ------------------------------------------------------------------------------------------- // TEST(ThreadPoolTaskPoolTest, HugeTasksAreNotRecycled) { TestTaskPool taskPool; { std::lock_guard callCountScope(CallCountMutex); std::size_t previousConstructorCallCount = TestTask::ConstructorCallCount; std::size_t previousDestructorCallCount = TestTask::DestructorCallCount; TestTask *originalTask = taskPool.GetNewTask(1024); EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 1); EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount); taskPool.ReturnTask(originalTask); EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 1); EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount + 1); TestTask *anotherTask = taskPool.GetNewTask(16); EXPECT_EQ(TestTask::ConstructorCallCount, previousConstructorCallCount + 2); EXPECT_EQ(TestTask::DestructorCallCount, previousDestructorCallCount + 1); // Cannot do this, C++ allocator might (and does, in practice) hand out // the new 16 byte task at the same address as the freed 1024 byte task. //EXPECT_NE(anotherTask, originalTask); EXPECT_GE(anotherTask->PayloadSize, 16U); taskPool.DeleteTask(anotherTask); } } // ------------------------------------------------------------------------------------------- // }}} // namespace Nuclex::Support::Threading