diff --git a/.gitignore b/.gitignore index 9309668d..a38f0748 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ /active_project /test/assets/assets.db /test/products/ +_codeql_detected_source_root diff --git a/cmake/thirdparty.cmake b/cmake/thirdparty.cmake index 00477be3..a26a9d98 100644 --- a/cmake/thirdparty.cmake +++ b/cmake/thirdparty.cmake @@ -37,7 +37,6 @@ if(EXISTS ${3RD_PATH}) sky_find_3rd(TARGET crc32 DIR crc32c) sky_find_3rd(TARGET sfmt DIR sfmt) sky_find_3rd(TARGET boost DIR boost) - sky_find_3rd(TARGET taskflow DIR taskflow) sky_find_3rd(TARGET mimalloc DIR mimalloc) # framework diff --git a/runtime/core/CMakeLists.txt b/runtime/core/CMakeLists.txt index d15acb19..fd569262 100644 --- a/runtime/core/CMakeLists.txt +++ b/runtime/core/CMakeLists.txt @@ -32,7 +32,6 @@ sky_add_library(TARGET Core STATIC LINK_LIBS 3rdParty::sfmt 3rdParty::crc32 - 3rdParty::taskflow 3rdParty::boost 3rdParty::rapidjson # 3rdParty::mimalloc diff --git a/runtime/core/include/core/async/NamedThread.h b/runtime/core/include/core/async/NamedThread.h index 6f5910ea..573132ee 100644 --- a/runtime/core/include/core/async/NamedThread.h +++ b/runtime/core/include/core/async/NamedThread.h @@ -4,8 +4,7 @@ #pragma once -#include - +#include #include #include #include @@ -27,7 +26,7 @@ namespace sky { void Signal(); private: - tf::Executor executor; + ThreadPool executor; Semaphore semaphore; }; diff --git a/runtime/core/include/core/async/Task.h b/runtime/core/include/core/async/Task.h index ed45c098..7fc8f15e 100644 --- a/runtime/core/include/core/async/Task.h +++ b/runtime/core/include/core/async/Task.h @@ -6,7 +6,7 @@ #include #include -#include +#include namespace sky { @@ -24,7 +24,7 @@ namespace sky { bool IsWorking() const; void ResetTask(); - tf::AsyncTask GetTask() const { return handle; } + AsyncTaskHandle GetTask() const { return handle; } protected: virtual bool DoWork() = 0; @@ -33,9 +33,9 @@ namespace sky { friend class TaskExecutor; - std::atomic_bool isDone{false}; - tf::AsyncTask handle; - std::vector dependencies; + std::atomic_bool isDone{false}; + AsyncTaskHandle handle; + std::vector dependencies; }; @@ -47,9 +47,15 @@ namespace sky { void WaitForAll(); - tf::Executor &GetExecutor() { return executor; } + ThreadPool &GetExecutor() { return executor; } private: - tf::Executor executor; + ThreadPool executor; }; + template + void ParallelFor(Index begin, Index end, Func &&func) + { + TaskExecutor::Get()->GetExecutor().parallel_for(begin, end, std::forward(func)); + } + } // namespace sky diff --git a/runtime/core/include/core/async/ThreadPool.h b/runtime/core/include/core/async/ThreadPool.h new file mode 100644 index 00000000..71710fdc --- /dev/null +++ b/runtime/core/include/core/async/ThreadPool.h @@ -0,0 +1,224 @@ +// +// Created by blues on 2024/9/6. +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sky { + + class AsyncTaskHandle { + public: + AsyncTaskHandle() = default; + + explicit AsyncTaskHandle(std::shared_future f) + : future(std::move(f)) + { + } + + bool empty() const { return !future.valid(); } + + void reset() { future = {}; } + + void Wait() const + { + if (future.valid()) { + future.wait(); + } + } + + bool IsReady() const + { + return !future.valid() || + future.wait_for(std::chrono::seconds(0)) == std::future_status::ready; + } + + private: + friend class ThreadPool; + std::shared_future future; + }; + + class ThreadPool { + public: + explicit ThreadPool(size_t numThreads = std::thread::hardware_concurrency()); + ~ThreadPool(); + + // Submit a task and get a handle back + template + AsyncTaskHandle async(Func &&func) + { + auto task = std::make_shared>(std::forward(func)); + std::shared_future future = task->get_future().share(); + + taskCount.fetch_add(1, std::memory_order_relaxed); + { + std::lock_guard lock(mutex); + tasks.emplace([task]() { (*task)(); }); + } + condition.notify_one(); + return AsyncTaskHandle(future); + } + + // Submit a task without returning a handle (fire and forget) + template + void silent_async(Func &&func) + { + taskCount.fetch_add(1, std::memory_order_relaxed); + { + std::lock_guard lock(mutex); + tasks.emplace([fn = std::forward(func)]() mutable { + fn(); + }); + } + condition.notify_one(); + } + + // Submit a task that depends on other async tasks (variadic) + template + std::pair> dependent_async(Func &&func, Tasks &&...deps) + { + std::vector depVec; + (depVec.emplace_back(std::forward(deps)), ...); + return dependent_async_impl(std::forward(func), depVec); + } + + // Submit a task that depends on other async tasks (range) + template + std::pair> dependent_async(Func &&func, Iter begin, Iter end) + { + std::vector deps(begin, end); + return dependent_async_impl(std::forward(func), deps); + } + + // Submit a dependent task without returning a future (fire and forget) + template + AsyncTaskHandle silent_dependent_async(Func &&func, Tasks &&...deps) + { + std::vector depVec; + (depVec.emplace_back(std::forward(deps)), ...); + return silent_dependent_async_impl(std::forward(func), depVec); + } + + // Parallel for loop: distributes [begin, end) across worker threads, calling func(index) for each index. + // Blocks until all iterations complete. + template + void parallel_for(Index begin, Index end, Func &&func) + { + if (begin >= end) { + return; + } + + const auto count = static_cast(end - begin); + const auto numWorkers = workers.size(); + + if (numWorkers == 0 || count == 1) { + for (Index i = begin; i < end; ++i) { + func(i); + } + return; + } + + const auto chunks = std::min(count, numWorkers); + const auto chunkSize = count / chunks; + const auto remainder = count % chunks; + + std::vector handles; + handles.reserve(chunks); + + Index offset = begin; + for (size_t c = 0; c < chunks; ++c) { + Index chunkBegin = offset; + Index chunkEnd = chunkBegin + static_cast(chunkSize + (c < remainder ? 1 : 0)); + offset = chunkEnd; + + handles.emplace_back(async([&func, chunkBegin, chunkEnd]() { + for (Index i = chunkBegin; i < chunkEnd; ++i) { + func(i); + } + })); + } + + for (auto &h : handles) { + h.Wait(); + } + } + + // Wait for all submitted tasks to complete + void wait_for_all(); + + size_t NumWorkers() const { return workers.size(); } + + private: + template + std::pair> dependent_async_impl(Func &&func, std::vector &deps) + { + auto task = std::make_shared>( + [fn = std::forward(func), dependencies = std::move(deps)]() mutable { + for (auto &dep : dependencies) { + dep.Wait(); + } + fn(); + }); + + std::future rawFuture = task->get_future(); + std::shared_future sharedFuture = rawFuture.share(); + AsyncTaskHandle handle(sharedFuture); + + taskCount.fetch_add(1, std::memory_order_relaxed); + { + std::lock_guard lock(mutex); + tasks.emplace([task]() { (*task)(); }); + } + condition.notify_one(); + + auto resultFuture = std::async(std::launch::deferred, [sharedFuture]() { + sharedFuture.wait(); + }); + + return {handle, std::move(resultFuture)}; + } + + template + AsyncTaskHandle silent_dependent_async_impl(Func &&func, std::vector &deps) + { + auto task = std::make_shared>( + [fn = std::forward(func), dependencies = std::move(deps)]() mutable { + for (auto &dep : dependencies) { + dep.Wait(); + } + fn(); + }); + + std::shared_future future = task->get_future().share(); + + taskCount.fetch_add(1, std::memory_order_relaxed); + { + std::lock_guard lock(mutex); + tasks.emplace([task]() { (*task)(); }); + } + condition.notify_one(); + return AsyncTaskHandle(future); + } + + void workerLoop(); + + std::vector workers; + std::queue> tasks; + std::mutex mutex; + std::condition_variable condition; + std::atomic stopped{false}; + std::atomic taskCount{0}; + std::mutex waitMutex; + std::condition_variable waitCondition; + }; + +} // namespace sky diff --git a/runtime/core/src/async/ThreadPool.cpp b/runtime/core/src/async/ThreadPool.cpp new file mode 100644 index 00000000..089ce905 --- /dev/null +++ b/runtime/core/src/async/ThreadPool.cpp @@ -0,0 +1,65 @@ +// +// Created by blues on 2024/9/6. +// + +#include + +namespace sky { + + ThreadPool::ThreadPool(size_t numThreads) + { + for (size_t i = 0; i < numThreads; ++i) { + workers.emplace_back([this]() { workerLoop(); }); + } + } + + ThreadPool::~ThreadPool() + { + { + std::lock_guard lock(mutex); + stopped.store(true, std::memory_order_relaxed); + } + condition.notify_all(); + for (auto &worker : workers) { + if (worker.joinable()) { + worker.join(); + } + } + } + + void ThreadPool::workerLoop() + { + while (true) { + std::function task; + { + std::unique_lock lock(mutex); + condition.wait(lock, [this]() { + return stopped.load(std::memory_order_relaxed) || !tasks.empty(); + }); + + if (stopped.load(std::memory_order_relaxed) && tasks.empty()) { + return; + } + + task = std::move(tasks.front()); + tasks.pop(); + } + + task(); + + if (taskCount.fetch_sub(1, std::memory_order_acq_rel) == 1) { + std::lock_guard lock(waitMutex); + waitCondition.notify_all(); + } + } + } + + void ThreadPool::wait_for_all() + { + std::unique_lock lock(waitMutex); + waitCondition.wait(lock, [this]() { + return taskCount.load(std::memory_order_acquire) == 0; + }); + } + +} // namespace sky diff --git a/runtime/framework/include/framework/asset/Asset.h b/runtime/framework/include/framework/asset/Asset.h index 196a4e0d..a8636acb 100644 --- a/runtime/framework/include/framework/asset/Asset.h +++ b/runtime/framework/include/framework/asset/Asset.h @@ -8,11 +8,10 @@ #include #include #include +#include #include #include -#include - #include #include #include @@ -23,7 +22,7 @@ namespace sky { class AssetBase; using AssetPtr = std::shared_ptr; - using AsyncTask = std::pair>; + using AsyncTask = std::pair>; class AssetBase { public: diff --git a/runtime/framework/include/framework/asset/AssetExecutor.h b/runtime/framework/include/framework/asset/AssetExecutor.h index 219e5aeb..edfb1145 100644 --- a/runtime/framework/include/framework/asset/AssetExecutor.h +++ b/runtime/framework/include/framework/asset/AssetExecutor.h @@ -4,7 +4,7 @@ #pragma once -#include +#include #include #include @@ -16,7 +16,7 @@ namespace sky { struct SavingTask { FilePtr file; - tf::AsyncTask asyncTask; + AsyncTaskHandle asyncTask; }; class AssetExecutor : public Singleton { @@ -66,7 +66,7 @@ namespace sky { void WaitForAll(); private: - tf::Executor executor; + ThreadPool executor; mutable std::mutex mutex; std::list savingTasks; diff --git a/runtime/framework/src/asset/AssetManager.cpp b/runtime/framework/src/asset/AssetManager.cpp index 54b4b440..bff041f1 100644 --- a/runtime/framework/src/asset/AssetManager.cpp +++ b/runtime/framework/src/asset/AssetManager.cpp @@ -103,7 +103,7 @@ namespace sky { // avoid release dep asset std::vector holder; - std::vector asyncTasks; + std::vector asyncTasks; holder.reserve(asset->dependencies.size()); for (auto &dep : asset->dependencies) { diff --git a/runtime/render/core/include/render/rdg/RenderGraphContext.h b/runtime/render/core/include/render/rdg/RenderGraphContext.h index 0870004e..cb6f13d3 100644 --- a/runtime/render/core/include/render/rdg/RenderGraphContext.h +++ b/runtime/render/core/include/render/rdg/RenderGraphContext.h @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include #include #include @@ -34,7 +34,7 @@ namespace sky::rdg { { } - tf::Executor executor; + ThreadPool executor; PmrUnSyncPoolRes resources; std::unique_ptr pool; diff --git a/runtime/render/shader/include/shader/ShaderFileSystem.h b/runtime/render/shader/include/shader/ShaderFileSystem.h index 3204d993..3c68fc91 100644 --- a/runtime/render/shader/include/shader/ShaderFileSystem.h +++ b/runtime/render/shader/include/shader/ShaderFileSystem.h @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include namespace sky { @@ -45,7 +45,7 @@ namespace sky { NativeFileSystemPtr cacheSourceFS; NativeFileSystemPtr compiledBinaryFS[static_cast(ShaderCompileTarget::NUM)]; - tf::Executor executor; + ThreadPool executor; }; } // namespace sky diff --git a/test/core/TaskTest.cpp b/test/core/TaskTest.cpp index 79f04270..6ff63f7f 100644 --- a/test/core/TaskTest.cpp +++ b/test/core/TaskTest.cpp @@ -36,4 +36,75 @@ TEST(TaskTest, TaskTestBase) task->StartAsync(); TaskExecutor::Get()->GetExecutor().wait_for_all(); ASSERT_EQ(id, 20); +} + +TEST(TaskTest, ParallelForBasic) +{ + const int N = 100; + std::vector v(N, 0); + + ThreadPool pool(4); + pool.parallel_for(0, N, [&v](int i) { + v[i] = i * 2; + }); + + for (int i = 0; i < N; ++i) { + ASSERT_EQ(v[i], i * 2); + } +} + +TEST(TaskTest, ParallelForEmpty) +{ + ThreadPool pool(4); + // Should not crash or hang + pool.parallel_for(0, 0, [](int) { + FAIL() << "Should not be called"; + }); + + pool.parallel_for(5, 5, [](int) { + FAIL() << "Should not be called"; + }); + + pool.parallel_for(10, 5, [](int) { + FAIL() << "Should not be called"; + }); +} + +TEST(TaskTest, ParallelForSingleElement) +{ + int value = 0; + ThreadPool pool(4); + pool.parallel_for(0, 1, [&value](int) { + value = 42; + }); + ASSERT_EQ(value, 42); +} + +TEST(TaskTest, ParallelForLargeRange) +{ + const int N = 10000; + std::vector v(N, 0); + + ThreadPool pool(4); + pool.parallel_for(0, N, [&v](int i) { + v[i] = i; + }); + + for (int i = 0; i < N; ++i) { + ASSERT_EQ(v[i], i); + } +} + +TEST(TaskTest, ParallelForConvenience) +{ + const int N = 100; + std::vector v(N, 0); + + ParallelFor(0, N, [&v](int i) { + v[i] = i + 1; + }); + + for (int i = 0; i < N; ++i) { + ASSERT_EQ(v[i], i + 1); + } } \ No newline at end of file