util/parallel.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/util/parallel.h" | ||
| 2 | |||
| 3 | #include <algorithm> | ||
| 4 | #include <atomic> | ||
| 5 | #include <condition_variable> | ||
| 6 | #include <deque> | ||
| 7 | #include <exception> | ||
| 8 | #include <memory> | ||
| 9 | #include <mutex> | ||
| 10 | #include <thread> | ||
| 11 | #include <vector> | ||
| 12 | |||
| 13 | namespace terraingen { | ||
| 14 | |||
| 15 | namespace { | ||
| 16 | |||
| 17 | /// One `ParallelFor` call: its tasks, handed out in order to the caller and its helpers. | ||
| 18 | struct Job { | ||
| 19 | std::size_t count = 0; | ||
| 20 | const std::function<void(std::size_t)>* task = nullptr; | ||
| 21 | std::atomic<std::size_t> next{0}; | ||
| 22 | std::atomic<int> slots{0}; | ||
| 23 | std::atomic<bool> failed{false}; | ||
| 24 | std::mutex mutex; | ||
| 25 | std::condition_variable idle; | ||
| 26 | int active = 0; | ||
| 27 | std::exception_ptr error; | ||
| 28 | |||
| 29 | /// Runs tasks until none are left or one has failed. | ||
| 30 | 2940 | void Work() { | |
| 31 | 208271 | while (!failed.load()) { | |
| 32 | 205328 | const std::size_t i = next.fetch_add(1); | |
| 33 | 205328 | if (i >= count) { | |
| 34 | 2937 | return; | |
| 35 | } | ||
| 36 | try { | ||
| 37 | 202391 | (*task)(i); | |
| 38 | 2 | } catch (...) { | |
| 39 | 2 | const std::lock_guard lock(mutex); | |
| 40 | 2 | if (!error) { | |
| 41 | 2 | error = std::current_exception(); | |
| 42 | } | ||
| 43 | 2 | failed.store(true); | |
| 44 | 2 | } | |
| 45 | } | ||
| 46 | } | ||
| 47 | |||
| 48 | /// Whether helpers have nothing left to take. | ||
| 49 | 2936 | bool Exhausted() const { | |
| 50 | 5870 | return failed.load() || next.load() >= count; | |
| 51 | } | ||
| 52 | }; | ||
| 53 | |||
| 54 | /// Threads kept for the life of the program, helping whichever jobs are waiting. The pool is | ||
| 55 | /// never destroyed: joining threads while statics are torn down can deadlock, as when a library is | ||
| 56 | /// unloaded on Windows, and the threads end with the process. | ||
| 57 | class Pool { | ||
| 58 | public: | ||
| 59 | 218 | Pool() { | |
| 60 | 218 | const unsigned size = std::max(1u, std::thread::hardware_concurrency()) - 1; | |
| 61 | 872 | for (unsigned t = 0; t < size; ++t) { | |
| 62 | 1296 | std::thread([this] { Loop(); }).detach(); | |
| 63 | } | ||
| 64 | 218 | } | |
| 65 | |||
| 66 | Pool(const Pool&) = delete; | ||
| 67 | Pool& operator=(const Pool&) = delete; | ||
| 68 | |||
| 69 | /// Offers `job` to the pool's threads. | ||
| 70 | 1040 | void Post(const std::shared_ptr<Job>& job) { | |
| 71 | { | ||
| 72 | 1040 | const std::lock_guard lock(mutex_); | |
| 73 | 1040 | jobs_.push_back(job); | |
| 74 | 1040 | } | |
| 75 | 1040 | wake_.notify_all(); | |
| 76 | 1040 | } | |
| 77 | |||
| 78 | private: | ||
| 79 | 642 | void Loop() { | |
| 80 | 642 | std::unique_lock lock(mutex_); | |
| 81 | while (true) { | ||
| 82 | 9130 | wake_.wait(lock, [this] { return !jobs_.empty(); }); | |
| 83 | 2936 | std::shared_ptr<Job> job = jobs_.front(); | |
| 84 | 4905 | if (job->Exhausted() || job->slots.fetch_sub(1) <= 0) { | |
| 85 | 1036 | jobs_.pop_front(); | |
| 86 | 1036 | continue; | |
| 87 | } | ||
| 88 | 1900 | lock.unlock(); | |
| 89 | { | ||
| 90 | 1900 | const std::lock_guard jobLock(job->mutex); | |
| 91 | 1900 | ++job->active; | |
| 92 | 1900 | } | |
| 93 | 1900 | job->Work(); | |
| 94 | { | ||
| 95 | 1900 | const std::lock_guard jobLock(job->mutex); | |
| 96 | 1900 | --job->active; | |
| 97 | 1900 | } | |
| 98 | 1900 | job->idle.notify_all(); | |
| 99 | 1895 | lock.lock(); | |
| 100 | 5862 | } | |
| 101 | } | ||
| 102 | |||
| 103 | std::mutex mutex_; | ||
| 104 | std::condition_variable wake_; | ||
| 105 | std::deque<std::shared_ptr<Job>> jobs_; | ||
| 106 | }; | ||
| 107 | |||
| 108 | 1040 | Pool& SharedPool() { | |
| 109 | 1040 | static Pool* const pool = new Pool; | |
| 110 | 1040 | return *pool; | |
| 111 | } | ||
| 112 | |||
| 113 | } // namespace | ||
| 114 | |||
| 115 | 149837 | unsigned ResolveThreads(unsigned threads) { | |
| 116 | 149837 | if (threads > 0) { | |
| 117 | 103331 | return threads; | |
| 118 | } | ||
| 119 | 46506 | return std::max(1u, std::thread::hardware_concurrency()); | |
| 120 | } | ||
| 121 | |||
| 122 | 149835 | void ParallelFor(std::size_t count, unsigned threads, | |
| 123 | const std::function<void(std::size_t index)>& task) { | ||
| 124 | 149835 | const std::size_t workers = std::min<std::size_t>(ResolveThreads(threads), count); | |
| 125 | 149835 | if (workers <= 1) { | |
| 126 | 311091 | for (std::size_t i = 0; i < count; ++i) { | |
| 127 | 162297 | task(i); | |
| 128 | } | ||
| 129 | 148794 | return; | |
| 130 | } | ||
| 131 | 1040 | auto job = std::make_shared<Job>(); | |
| 132 | 1040 | job->count = count; | |
| 133 | 1040 | job->task = &task; | |
| 134 | 1040 | job->slots.store(static_cast<int>(workers - 1)); | |
| 135 | 1040 | SharedPool().Post(job); | |
| 136 | 1040 | job->Work(); | |
| 137 | { | ||
| 138 | 1040 | std::unique_lock lock(job->mutex); | |
| 139 | 2884 | job->idle.wait(lock, [&] { return job->active == 0; }); | |
| 140 | 1040 | } | |
| 141 | 1040 | job->slots.store(0); | |
| 142 | 1040 | if (job->error) { | |
| 143 | 4 | std::rethrow_exception(job->error); | |
| 144 | } | ||
| 145 | 1040 | } | |
| 146 | |||
| 147 | } // namespace terraingen | ||
| 148 |