diff options
| author | 2020-02-05 15:48:20 -0400 | |
|---|---|---|
| committer | 2020-06-18 16:29:15 -0400 | |
| commit | be320a9e10fda32a984b12cdfe3aaf09cc67b39a (patch) | |
| tree | f2bb28be6adc2a416b59393bb8f438636e9c79c1 | |
| parent | Tests: Add tests for fibers and refactor/fix Fiber class (diff) | |
| download | yuzu-be320a9e10fda32a984b12cdfe3aaf09cc67b39a.tar.gz yuzu-be320a9e10fda32a984b12cdfe3aaf09cc67b39a.tar.xz yuzu-be320a9e10fda32a984b12cdfe3aaf09cc67b39a.zip | |
Common: Polish Fiber class, add comments, asserts and more tests.
| -rw-r--r-- | src/common/fiber.cpp | 55 | ||||
| -rw-r--r-- | src/common/fiber.h | 14 | ||||
| -rw-r--r-- | src/common/spin_lock.cpp | 7 | ||||
| -rw-r--r-- | src/common/spin_lock.h | 1 | ||||
| -rw-r--r-- | src/tests/common/fibers.cpp | 95 |
5 files changed, 147 insertions, 25 deletions
diff --git a/src/common/fiber.cpp b/src/common/fiber.cpp index a2c0401c4..a88a30ced 100644 --- a/src/common/fiber.cpp +++ b/src/common/fiber.cpp | |||
| @@ -2,6 +2,7 @@ | |||
| 2 | // Licensed under GPLv2 or any later version | 2 | // Licensed under GPLv2 or any later version |
| 3 | // Refer to the license.txt file included. | 3 | // Refer to the license.txt file included. |
| 4 | 4 | ||
| 5 | #include "common/assert.h" | ||
| 5 | #include "common/fiber.h" | 6 | #include "common/fiber.h" |
| 6 | #ifdef _MSC_VER | 7 | #ifdef _MSC_VER |
| 7 | #include <windows.h> | 8 | #include <windows.h> |
| @@ -18,11 +19,11 @@ struct Fiber::FiberImpl { | |||
| 18 | }; | 19 | }; |
| 19 | 20 | ||
| 20 | void Fiber::start() { | 21 | void Fiber::start() { |
| 21 | if (previous_fiber) { | 22 | ASSERT(previous_fiber != nullptr); |
| 22 | previous_fiber->guard.unlock(); | 23 | previous_fiber->guard.unlock(); |
| 23 | previous_fiber = nullptr; | 24 | previous_fiber.reset(); |
| 24 | } | ||
| 25 | entry_point(start_parameter); | 25 | entry_point(start_parameter); |
| 26 | UNREACHABLE(); | ||
| 26 | } | 27 | } |
| 27 | 28 | ||
| 28 | void __stdcall Fiber::FiberStartFunc(void* fiber_parameter) | 29 | void __stdcall Fiber::FiberStartFunc(void* fiber_parameter) |
| @@ -43,12 +44,16 @@ Fiber::Fiber() : guard{}, entry_point{}, start_parameter{}, previous_fiber{} { | |||
| 43 | 44 | ||
| 44 | Fiber::~Fiber() { | 45 | Fiber::~Fiber() { |
| 45 | // Make sure the Fiber is not being used | 46 | // Make sure the Fiber is not being used |
| 46 | guard.lock(); | 47 | bool locked = guard.try_lock(); |
| 47 | guard.unlock(); | 48 | ASSERT_MSG(locked, "Destroying a fiber that's still running"); |
| 49 | if (locked) { | ||
| 50 | guard.unlock(); | ||
| 51 | } | ||
| 48 | DeleteFiber(impl->handle); | 52 | DeleteFiber(impl->handle); |
| 49 | } | 53 | } |
| 50 | 54 | ||
| 51 | void Fiber::Exit() { | 55 | void Fiber::Exit() { |
| 56 | ASSERT_MSG(is_thread_fiber, "Exitting non main thread fiber"); | ||
| 52 | if (!is_thread_fiber) { | 57 | if (!is_thread_fiber) { |
| 53 | return; | 58 | return; |
| 54 | } | 59 | } |
| @@ -57,14 +62,15 @@ void Fiber::Exit() { | |||
| 57 | } | 62 | } |
| 58 | 63 | ||
| 59 | void Fiber::YieldTo(std::shared_ptr<Fiber> from, std::shared_ptr<Fiber> to) { | 64 | void Fiber::YieldTo(std::shared_ptr<Fiber> from, std::shared_ptr<Fiber> to) { |
| 65 | ASSERT_MSG(from != nullptr, "Yielding fiber is null!"); | ||
| 66 | ASSERT_MSG(to != nullptr, "Next fiber is null!"); | ||
| 60 | to->guard.lock(); | 67 | to->guard.lock(); |
| 61 | to->previous_fiber = from; | 68 | to->previous_fiber = from; |
| 62 | SwitchToFiber(to->impl->handle); | 69 | SwitchToFiber(to->impl->handle); |
| 63 | auto previous_fiber = from->previous_fiber; | 70 | auto previous_fiber = from->previous_fiber; |
| 64 | if (previous_fiber) { | 71 | ASSERT(previous_fiber != nullptr); |
| 65 | previous_fiber->guard.unlock(); | 72 | previous_fiber->guard.unlock(); |
| 66 | previous_fiber.reset(); | 73 | previous_fiber.reset(); |
| 67 | } | ||
| 68 | } | 74 | } |
| 69 | 75 | ||
| 70 | std::shared_ptr<Fiber> Fiber::ThreadToFiber() { | 76 | std::shared_ptr<Fiber> Fiber::ThreadToFiber() { |
| @@ -85,12 +91,12 @@ struct alignas(64) Fiber::FiberImpl { | |||
| 85 | }; | 91 | }; |
| 86 | 92 | ||
| 87 | void Fiber::start(boost::context::detail::transfer_t& transfer) { | 93 | void Fiber::start(boost::context::detail::transfer_t& transfer) { |
| 88 | if (previous_fiber) { | 94 | ASSERT(previous_fiber != nullptr); |
| 89 | previous_fiber->impl->context = transfer.fctx; | 95 | previous_fiber->impl->context = transfer.fctx; |
| 90 | previous_fiber->guard.unlock(); | 96 | previous_fiber->guard.unlock(); |
| 91 | previous_fiber = nullptr; | 97 | previous_fiber.reset(); |
| 92 | } | ||
| 93 | entry_point(start_parameter); | 98 | entry_point(start_parameter); |
| 99 | UNREACHABLE(); | ||
| 94 | } | 100 | } |
| 95 | 101 | ||
| 96 | void Fiber::FiberStartFunc(boost::context::detail::transfer_t transfer) | 102 | void Fiber::FiberStartFunc(boost::context::detail::transfer_t transfer) |
| @@ -113,11 +119,15 @@ Fiber::Fiber() : guard{}, entry_point{}, start_parameter{}, previous_fiber{} { | |||
| 113 | 119 | ||
| 114 | Fiber::~Fiber() { | 120 | Fiber::~Fiber() { |
| 115 | // Make sure the Fiber is not being used | 121 | // Make sure the Fiber is not being used |
| 116 | guard.lock(); | 122 | bool locked = guard.try_lock(); |
| 117 | guard.unlock(); | 123 | ASSERT_MSG(locked, "Destroying a fiber that's still running"); |
| 124 | if (locked) { | ||
| 125 | guard.unlock(); | ||
| 126 | } | ||
| 118 | } | 127 | } |
| 119 | 128 | ||
| 120 | void Fiber::Exit() { | 129 | void Fiber::Exit() { |
| 130 | ASSERT_MSG(is_thread_fiber, "Exitting non main thread fiber"); | ||
| 121 | if (!is_thread_fiber) { | 131 | if (!is_thread_fiber) { |
| 122 | return; | 132 | return; |
| 123 | } | 133 | } |
| @@ -125,15 +135,16 @@ void Fiber::Exit() { | |||
| 125 | } | 135 | } |
| 126 | 136 | ||
| 127 | void Fiber::YieldTo(std::shared_ptr<Fiber> from, std::shared_ptr<Fiber> to) { | 137 | void Fiber::YieldTo(std::shared_ptr<Fiber> from, std::shared_ptr<Fiber> to) { |
| 138 | ASSERT_MSG(from != nullptr, "Yielding fiber is null!"); | ||
| 139 | ASSERT_MSG(to != nullptr, "Next fiber is null!"); | ||
| 128 | to->guard.lock(); | 140 | to->guard.lock(); |
| 129 | to->previous_fiber = from; | 141 | to->previous_fiber = from; |
| 130 | auto transfer = boost::context::detail::jump_fcontext(to->impl.context, nullptr); | 142 | auto transfer = boost::context::detail::jump_fcontext(to->impl.context, nullptr); |
| 131 | auto previous_fiber = from->previous_fiber; | 143 | auto previous_fiber = from->previous_fiber; |
| 132 | if (previous_fiber) { | 144 | ASSERT(previous_fiber != nullptr); |
| 133 | previous_fiber->impl->context = transfer.fctx; | 145 | previous_fiber->impl->context = transfer.fctx; |
| 134 | previous_fiber->guard.unlock(); | 146 | previous_fiber->guard.unlock(); |
| 135 | previous_fiber.reset(); | 147 | previous_fiber.reset(); |
| 136 | } | ||
| 137 | } | 148 | } |
| 138 | 149 | ||
| 139 | std::shared_ptr<Fiber> Fiber::ThreadToFiber() { | 150 | std::shared_ptr<Fiber> Fiber::ThreadToFiber() { |
diff --git a/src/common/fiber.h b/src/common/fiber.h index 812d6644a..89a01fdd8 100644 --- a/src/common/fiber.h +++ b/src/common/fiber.h | |||
| @@ -18,6 +18,18 @@ namespace boost::context::detail { | |||
| 18 | 18 | ||
| 19 | namespace Common { | 19 | namespace Common { |
| 20 | 20 | ||
| 21 | /** | ||
| 22 | * Fiber class | ||
| 23 | * a fiber is a userspace thread with it's own context. They can be used to | ||
| 24 | * implement coroutines, emulated threading systems and certain asynchronous | ||
| 25 | * patterns. | ||
| 26 | * | ||
| 27 | * This class implements fibers at a low level, thus allowing greater freedom | ||
| 28 | * to implement such patterns. This fiber class is 'threadsafe' only one fiber | ||
| 29 | * can be running at a time and threads will be locked while trying to yield to | ||
| 30 | * a running fiber until it yields. WARNING exchanging two running fibers between | ||
| 31 | * threads will cause a deadlock. | ||
| 32 | */ | ||
| 21 | class Fiber { | 33 | class Fiber { |
| 22 | public: | 34 | public: |
| 23 | Fiber(std::function<void(void*)>&& entry_point_func, void* start_parameter); | 35 | Fiber(std::function<void(void*)>&& entry_point_func, void* start_parameter); |
| @@ -53,8 +65,6 @@ private: | |||
| 53 | static void FiberStartFunc(boost::context::detail::transfer_t transfer); | 65 | static void FiberStartFunc(boost::context::detail::transfer_t transfer); |
| 54 | #endif | 66 | #endif |
| 55 | 67 | ||
| 56 | |||
| 57 | |||
| 58 | struct FiberImpl; | 68 | struct FiberImpl; |
| 59 | 69 | ||
| 60 | SpinLock guard; | 70 | SpinLock guard; |
diff --git a/src/common/spin_lock.cpp b/src/common/spin_lock.cpp index 8077b78d2..82a1d39ff 100644 --- a/src/common/spin_lock.cpp +++ b/src/common/spin_lock.cpp | |||
| @@ -43,4 +43,11 @@ void SpinLock::unlock() { | |||
| 43 | lck.clear(std::memory_order_release); | 43 | lck.clear(std::memory_order_release); |
| 44 | } | 44 | } |
| 45 | 45 | ||
| 46 | bool SpinLock::try_lock() { | ||
| 47 | if (lck.test_and_set(std::memory_order_acquire)) { | ||
| 48 | return false; | ||
| 49 | } | ||
| 50 | return true; | ||
| 51 | } | ||
| 52 | |||
| 46 | } // namespace Common | 53 | } // namespace Common |
diff --git a/src/common/spin_lock.h b/src/common/spin_lock.h index cbc67b6c8..70282a961 100644 --- a/src/common/spin_lock.h +++ b/src/common/spin_lock.h | |||
| @@ -12,6 +12,7 @@ class SpinLock { | |||
| 12 | public: | 12 | public: |
| 13 | void lock(); | 13 | void lock(); |
| 14 | void unlock(); | 14 | void unlock(); |
| 15 | bool try_lock(); | ||
| 15 | 16 | ||
| 16 | private: | 17 | private: |
| 17 | std::atomic_flag lck = ATOMIC_FLAG_INIT; | 18 | std::atomic_flag lck = ATOMIC_FLAG_INIT; |
diff --git a/src/tests/common/fibers.cpp b/src/tests/common/fibers.cpp index ff840afa6..358393a19 100644 --- a/src/tests/common/fibers.cpp +++ b/src/tests/common/fibers.cpp | |||
| @@ -64,7 +64,9 @@ static void ThreadStart1(u32 id, TestControl1& test_control) { | |||
| 64 | test_control.ExecuteThread(id); | 64 | test_control.ExecuteThread(id); |
| 65 | } | 65 | } |
| 66 | 66 | ||
| 67 | 67 | /** This test checks for fiber setup configuration and validates that fibers are | |
| 68 | * doing all the work required. | ||
| 69 | */ | ||
| 68 | TEST_CASE("Fibers::Setup", "[common]") { | 70 | TEST_CASE("Fibers::Setup", "[common]") { |
| 69 | constexpr u32 num_threads = 7; | 71 | constexpr u32 num_threads = 7; |
| 70 | TestControl1 test_control{}; | 72 | TestControl1 test_control{}; |
| @@ -188,6 +190,10 @@ static void ThreadStart2_2(u32 id, TestControl2& test_control) { | |||
| 188 | test_control.Exit(); | 190 | test_control.Exit(); |
| 189 | } | 191 | } |
| 190 | 192 | ||
| 193 | /** This test checks for fiber thread exchange configuration and validates that fibers are | ||
| 194 | * that a fiber has been succesfully transfered from one thread to another and that the TLS | ||
| 195 | * region of the thread is kept while changing fibers. | ||
| 196 | */ | ||
| 191 | TEST_CASE("Fibers::InterExchange", "[common]") { | 197 | TEST_CASE("Fibers::InterExchange", "[common]") { |
| 192 | TestControl2 test_control{}; | 198 | TestControl2 test_control{}; |
| 193 | test_control.thread_fibers.resize(2, nullptr); | 199 | test_control.thread_fibers.resize(2, nullptr); |
| @@ -210,5 +216,92 @@ TEST_CASE("Fibers::InterExchange", "[common]") { | |||
| 210 | REQUIRE(test_control.value1 == cal_value); | 216 | REQUIRE(test_control.value1 == cal_value); |
| 211 | } | 217 | } |
| 212 | 218 | ||
| 219 | class TestControl3 { | ||
| 220 | public: | ||
| 221 | TestControl3() = default; | ||
| 222 | |||
| 223 | void DoWork1() { | ||
| 224 | value1 += 1; | ||
| 225 | Fiber::YieldTo(fiber1, fiber2); | ||
| 226 | std::thread::id this_id = std::this_thread::get_id(); | ||
| 227 | u32 id = ids[this_id]; | ||
| 228 | value3 += 1; | ||
| 229 | Fiber::YieldTo(fiber1, thread_fibers[id]); | ||
| 230 | } | ||
| 231 | |||
| 232 | void DoWork2() { | ||
| 233 | value2 += 1; | ||
| 234 | std::thread::id this_id = std::this_thread::get_id(); | ||
| 235 | u32 id = ids[this_id]; | ||
| 236 | Fiber::YieldTo(fiber2, thread_fibers[id]); | ||
| 237 | } | ||
| 238 | |||
| 239 | void ExecuteThread(u32 id); | ||
| 240 | |||
| 241 | void CallFiber1() { | ||
| 242 | std::thread::id this_id = std::this_thread::get_id(); | ||
| 243 | u32 id = ids[this_id]; | ||
| 244 | Fiber::YieldTo(thread_fibers[id], fiber1); | ||
| 245 | } | ||
| 246 | |||
| 247 | void Exit(); | ||
| 248 | |||
| 249 | u32 value1{}; | ||
| 250 | u32 value2{}; | ||
| 251 | u32 value3{}; | ||
| 252 | std::unordered_map<std::thread::id, u32> ids; | ||
| 253 | std::vector<std::shared_ptr<Common::Fiber>> thread_fibers; | ||
| 254 | std::shared_ptr<Common::Fiber> fiber1; | ||
| 255 | std::shared_ptr<Common::Fiber> fiber2; | ||
| 256 | }; | ||
| 257 | |||
| 258 | static void WorkControl3_1(void* control) { | ||
| 259 | TestControl3* test_control = static_cast<TestControl3*>(control); | ||
| 260 | test_control->DoWork1(); | ||
| 261 | } | ||
| 262 | |||
| 263 | static void WorkControl3_2(void* control) { | ||
| 264 | TestControl3* test_control = static_cast<TestControl3*>(control); | ||
| 265 | test_control->DoWork2(); | ||
| 266 | } | ||
| 267 | |||
| 268 | void TestControl3::ExecuteThread(u32 id) { | ||
| 269 | std::thread::id this_id = std::this_thread::get_id(); | ||
| 270 | ids[this_id] = id; | ||
| 271 | auto thread_fiber = Fiber::ThreadToFiber(); | ||
| 272 | thread_fibers[id] = thread_fiber; | ||
| 273 | } | ||
| 274 | |||
| 275 | void TestControl3::Exit() { | ||
| 276 | std::thread::id this_id = std::this_thread::get_id(); | ||
| 277 | u32 id = ids[this_id]; | ||
| 278 | thread_fibers[id]->Exit(); | ||
| 279 | } | ||
| 280 | |||
| 281 | static void ThreadStart3(u32 id, TestControl3& test_control) { | ||
| 282 | test_control.ExecuteThread(id); | ||
| 283 | test_control.CallFiber1(); | ||
| 284 | test_control.Exit(); | ||
| 285 | } | ||
| 286 | |||
| 287 | /** This test checks for one two threads racing for starting the same fiber. | ||
| 288 | * It checks execution occured in an ordered manner and by no time there were | ||
| 289 | * two contexts at the same time. | ||
| 290 | */ | ||
| 291 | TEST_CASE("Fibers::StartRace", "[common]") { | ||
| 292 | TestControl3 test_control{}; | ||
| 293 | test_control.thread_fibers.resize(2, nullptr); | ||
| 294 | test_control.fiber1 = std::make_shared<Fiber>(std::function<void(void*)>{WorkControl3_1}, &test_control); | ||
| 295 | test_control.fiber2 = std::make_shared<Fiber>(std::function<void(void*)>{WorkControl3_2}, &test_control); | ||
| 296 | std::thread thread1(ThreadStart3, 0, std::ref(test_control)); | ||
| 297 | std::thread thread2(ThreadStart3, 1, std::ref(test_control)); | ||
| 298 | thread1.join(); | ||
| 299 | thread2.join(); | ||
| 300 | REQUIRE(test_control.value1 == 1); | ||
| 301 | REQUIRE(test_control.value2 == 1); | ||
| 302 | REQUIRE(test_control.value3 == 1); | ||
| 303 | } | ||
| 304 | |||
| 305 | |||
| 213 | 306 | ||
| 214 | } // namespace Common | 307 | } // namespace Common |