From: ljsebald <ljs...@us...> - 2023-11-18 15:21:02
|
This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "A pseudo Operating System for the Dreamcast.". The branch, master has been updated via f75efb895335970db3a4019f08457b196c985ae1 (commit) via 02b916e8094ed7e09309ea85f7f045fb3dd266c8 (commit) via 5fb12dea49ee7b9db9e27785b52ca56e2fa8b76f (commit) via 4e833ec168513ad709c093adc2ec32a585a2c1a3 (commit) from 932513b05e6f234ddde655a628addb01e9e75753 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit f75efb895335970db3a4019f08457b196c985ae1 Author: Falco Girgis <gyr...@gm...> Date: Sat Nov 18 09:20:17 2023 -0600 C++20 Concurrency Test/Example (#315) Add C++ 20 concurrency test/example. commit 02b916e8094ed7e09309ea85f7f045fb3dd266c8 Author: ross.codes <ros...@go...> Date: Sat Nov 18 15:18:17 2023 +0000 PVR: Add option to specify additional OPBs for TA overflow (#333) * Add option to specify additional OPBs for TA overflow * Fix default OPB allocation when OPB overflow is set to 0 commit 5fb12dea49ee7b9db9e27785b52ca56e2fa8b76f Merge: 932513b 4e833ec Author: Lawrence Sebald <ljs...@us...> Date: Sat Nov 18 10:16:41 2023 -0500 Merge pull request #364 from DC-SWAT/snd_thread_safe Thread-safe audio fixes commit 4e833ec168513ad709c093adc2ec32a585a2c1a3 Author: DC-SWAT <sw...@21...> Date: Thu Nov 16 16:57:06 2023 +0700 Thread-safe audio fixes ----------------------------------------------------------------------- Summary of changes: examples/dreamcast/cpp/Makefile | 5 +- examples/dreamcast/cpp/concurrency/Makefile | 29 + examples/dreamcast/cpp/concurrency/concurrency.cpp | 743 +++++++++++++++++++++ include/kos/opts.h | 3 + kernel/arch/dreamcast/hardware/pvr/pvr_buffers.c | 16 +- .../dreamcast/hardware/pvr/pvr_init_shutdown.c | 31 +- kernel/arch/dreamcast/hardware/pvr/pvr_internal.h | 4 + kernel/arch/dreamcast/hardware/pvr/pvr_irq.c | 39 +- kernel/arch/dreamcast/hardware/pvr/pvr_misc.c | 20 +- kernel/arch/dreamcast/hardware/vblank.c | 8 +- kernel/arch/dreamcast/include/dc/asic.h | 36 +- kernel/arch/dreamcast/include/dc/pvr.h | 13 +- kernel/arch/dreamcast/sound/snd_mem.c | 69 +- kernel/arch/dreamcast/sound/snd_stream.c | 19 +- 14 files changed, 988 insertions(+), 47 deletions(-) create mode 100644 examples/dreamcast/cpp/concurrency/Makefile create mode 100644 examples/dreamcast/cpp/concurrency/concurrency.cpp diff --git a/examples/dreamcast/cpp/Makefile b/examples/dreamcast/cpp/Makefile index d50bbcc..135a427 100644 --- a/examples/dreamcast/cpp/Makefile +++ b/examples/dreamcast/cpp/Makefile @@ -1,7 +1,7 @@ # KallistiOS ##version## # # examples/dreamcast/cpp/Makefile -# (c)2001-2002 Megan Potter +# Copyright (C) 2001-2002 Megan Potter # all: @@ -10,6 +10,7 @@ all: $(KOS_MAKE) -C clock $(KOS_MAKE) -C modplug_test $(KOS_MAKE) -C out_of_memory + $(KOS_MAKE) -C concurrency clean: $(KOS_MAKE) -C gltest clean @@ -17,6 +18,7 @@ clean: $(KOS_MAKE) -C clock clean $(KOS_MAKE) -C modplug_test clean $(KOS_MAKE) -C out_of_memory clean + $(KOS_MAKE) -C concurrency clean dist: $(KOS_MAKE) -C gltest dist @@ -24,5 +26,6 @@ dist: $(KOS_MAKE) -C clock dist $(KOS_MAKE) -C modplug_test dist $(KOS_MAKE) -C out_of_memory dist + $(KOS_MAKE) -C concurrency dist diff --git a/examples/dreamcast/cpp/concurrency/Makefile b/examples/dreamcast/cpp/concurrency/Makefile new file mode 100644 index 0000000..65c7765 --- /dev/null +++ b/examples/dreamcast/cpp/concurrency/Makefile @@ -0,0 +1,29 @@ +# +# C++ Concurrency Test/Example +# (c) 2023 Falco Girgis +# + +TARGET = concurrency.elf +OBJS = concurrency.o +KOS_CPPFLAGS += -std=c++20 + +all: rm-elf $(TARGET) + +include $(KOS_BASE)/Makefile.rules + +clean: + -rm -f $(TARGET) $(OBJS) + +rm-elf: + -rm -f $(TARGET) + +$(TARGET): $(OBJS) + kos-c++ -o $(TARGET) $(OBJS) -lm + +run: $(TARGET) + $(KOS_LOADER) $(TARGET) + +dist: + rm -f $(OBJS) + $(KOS_STRIP) $(TARGET) + diff --git a/examples/dreamcast/cpp/concurrency/concurrency.cpp b/examples/dreamcast/cpp/concurrency/concurrency.cpp new file mode 100644 index 0000000..e4892ae --- /dev/null +++ b/examples/dreamcast/cpp/concurrency/concurrency.cpp @@ -0,0 +1,743 @@ +/* KallistiOS ##version## + + examples/dreamcast/cpp/concurrency/concurrency.cpp + + Copyright (C) 2023 Falco Girgis + +*/ + +/* + This file serves as both an example of using and as validation test suite + for all of standard C++ concurrency, through C++20. It is composed of 8 + standalone test cases, which have been either created from scratch or have + been adopted from examples on cppreference.com. These test cases aim to + flex the various synchronization primitives and threading constructs, + demonstrating their usage and ensuring that KOS, the toolchain, and + libstdc++ are behaving properly. After the tests run, a final result + message will be displayed, indicating whether the test suite passed or + failed. + + The following constructs are tested: + - atomic + - thread_local + - async + - thread/jthread + - mutex + - shared_mutex + - unique_lock + - lock_guard + - future + - promise + - semaphore + - latch + - shared_lock + - condition_variable + - scoped_lock + - barrier + - stop_source + - stop_token + - coroutine + - syncstream +*/ + +#include <iostream> +#include <atomic> +#include <array> +#include <future> +#include <chrono> +#include <functional> +#include <coroutine> +#include <semaphore> +#include <thread> +#include <barrier> +#include <cstdlib> +#include <latch> +#include <syncstream> +#include <shared_mutex> +#include <condition_variable> +#include <stop_token> +#include <span> +#include <random> +#include <exception> +#include <string> + +#include <arch/wdt.h> + +// 20 seconds in us +inline constexpr unsigned WATCHDOG_TIMEOUT = (20 * 1000 * 1000); +// Number of threads to spawn -- each of which runs the entire test suite +inline constexpr int THREAD_COUNT = 10; + +using namespace std::chrono_literals; + +// Exception type for test-case errors +class TestCaseException: public std::exception { + std::string what_; + public: + TestCaseException(std::string str) noexcept: + what_(std::move(str)) {} + + TestCaseException& + operator=(const TestCaseException& other) noexcept = default; + + const char* what() const noexcept override { + return what_.c_str(); + } +}; + +/* ===== TEST CASE 1: std::binary_semaphore ===== + Simply spawns a worker thread and ping-pongs acquiring + and releasing two binary_semaphores between the worker and + parent thread. +*/ +static void run_semaphore(std::binary_semaphore &sem_main_to_thread, + std::binary_semaphore &sem_thread_to_main) { + // wait for a signal from the main proc + // by attempting to decrement the semaphore + sem_main_to_thread.acquire(); + + // this call blocks until the semaphore's count + // is increased from the main proc + + std::cout << "[BINARY_SEMAPHORE] thread: Got the signal" << std::endl; + + // wait for 3 seconds to imitate some work + // being done by the thread + std::this_thread::sleep_for(1ms); + + std::cout << "[BINARY_SEMAPHORE] thread: Send the signal" << std::endl; + + // signal the main proc back + sem_thread_to_main.release(); +} + +static void test_semaphore() { + std::binary_semaphore sem_main_to_thread{0}, sem_thread_to_main{0}; + std::cout << "[BINARY_SEMAPHORE] Starting test." << std::endl; + + // create some worker thread + std::thread thrWorker(run_semaphore, + std::ref(sem_main_to_thread), + std::ref(sem_thread_to_main)); + + std::cout << "[BINARY_SEMAPHORE] main: Send the signal" << std::endl; + + // signal the worker thread to start working + // by increasing the semaphore's count + sem_main_to_thread.release(); + + // wait until the worker thread is done doing the work + // by attempting to decrement the semaphore's count + sem_thread_to_main.acquire(); + + std::cout << "[BINARY_SEMAPHORE] main: Got the signal" << std::endl; + thrWorker.join(); + + std::cout << "[BINARY_SEMAPHORE] Finished test." << std::endl; +} + +/* ===== TEST CASE 2: std::latch ===== + Creates 3 "Job" objects, each of which gets + managed by its own thread. First, the main thread + waits until all job threads have hit a synchronization + point (creating work). Then the workers wait until + the main thread hits a synchronization point to begin + cleaning their work. +*/ +struct LatchJob { + const std::string name; + std::string product{"not worked"}; + std::thread action; +}; + +static void test_latch() { + LatchJob jobs[]{{"Sonic"}, {"Knuckles"}, {"Tails"}}; + + std::cout << "[LATCH] Starting test." << std::endl; + + std::latch work_done{std::size(jobs)}; + std::latch start_clean_up{1}; + + auto work = [&](LatchJob &my_job) { + my_job.product = my_job.name + " worked"; + work_done.count_down(); + start_clean_up.wait(); + my_job.product = my_job.name + " cleaned"; + }; + + std::cout << "[LATCH] Work is starting... " << std::endl; + for (auto &job : jobs) + job.action = std::thread{work, std::ref(job)}; + + work_done.wait(); + std::cout << "[LATCH] done." << std::endl; + for (auto const &job : jobs) + std::cout << "[LATCH] " << job.product << '\n'; + + std::cout << "[LATCH] Workers are cleaning up... "; + start_clean_up.count_down(); + for (auto &job : jobs) + job.action.join(); + + std::cout << "[LATCH] done." << std::endl; + for (auto const& job : jobs) { + if(job.product == "not worked") { + throw TestCaseException("[LATCH] Job failed to produce!"); + } + std::cout << "[LATCH] " << job.product << std::endl; + } + + std::cout << "[LATCH] Finished test." << std::endl; +} + +/* ===== TEST CASE 3: std::shared_lock ===== + Creates a SharedLockCounter in the parent thread, which then + gets passed to two child threads, both of which attempt to + increment its value 3 times and print it. Write operations + are protected via std::unique_locks, while the read operation, + get(), is protected by a std::shared_lock. This models a + traditional "ReadWrite" lock. +*/ + +class SharedLockCounter { +public: + SharedLockCounter() = default; + + // Multiple threads/readers can read the counter's value at the same time. + unsigned int get() const { + std::shared_lock lock(mutex_); + return value_; + } + + // Only one thread/writer can increment/write the counter's value. + void increment() { + std::unique_lock lock(mutex_); + ++value_; + } + + // Only one thread/writer can reset/write the counter's value. + void reset() { + std::unique_lock lock(mutex_); + value_ = 0; + } + +private: + mutable std::shared_mutex mutex_; + unsigned int value_{}; +}; + +static void test_shared_lock() { + SharedLockCounter counter; + + std::cout << "[SHARED_LOCK] Starting test." << std::endl; + + auto increment_and_print = [&counter] { + for(int i{}; i != 3; ++i) { + counter.increment(); + std::osyncstream(std::cout) + << "[SHARED_LOCK] " + << std::this_thread::get_id() << ' ' + << counter.get() << std::endl; + } + }; + + std::thread thread1(increment_and_print); + std::thread thread2(increment_and_print); + + thread1.join(); + thread2.join(); + + if(counter.get() != 6) + throw TestCaseException("[SHARED_LOCK]: Unexpected counter value!"); + + std::cout << "[SHARED_LOCK] Finished test." << std::endl; +} + +/* ===== TEST CASE 4: std::condition_variable ===== + Spawns a worker thread, passing it a state object with both a + mutex and a condition_variable. The parent thread populates the + data field within the state object then uses the condition_variable + to signal to one thread (the child) that its data is ready to process. + The parent thread then waits on the condition_variable, which the + child thread signals back when it is done processing its work data. + + Basically the parent and child threads swap between signalling to + each other to proceed execution via the condition_variable. +*/ +struct CondVarState { + std::mutex m; + std::condition_variable cv; + std::string data; + bool ready = false; + bool processed = false; +}; + +static void run_condition_variable(CondVarState &cond_variable_state) { + // Wait until main() sends data + std::unique_lock lk(cond_variable_state.m); + cond_variable_state.cv.wait(lk, [&]{ return cond_variable_state.ready; }); + + // after the wait, we own the lock. + std::cout << "[COND_VARIABLE]: Worker thread is processing data" << std::endl; + cond_variable_state.data += " after processing"; + + // Send data back to main() + cond_variable_state.processed = true; + std::cout << "[COND_VARIABLE]: Worker thread signals data " + "processing completed" << std::endl; + + // Manual unlocking is done before notifying, to avoid waking up + // the waiting thread only to block again (see notify_one for details) + lk.unlock(); + cond_variable_state.cv.notify_one(); +} + +static void test_condition_variable() { + CondVarState cond_variable_state; + + std::cout << "[COND_VARIABLE] Starting test." << std::endl; + + std::thread worker(run_condition_variable, std::ref(cond_variable_state)); + + cond_variable_state.data = "Example data"; + // send data to the worker thread + { + std::lock_guard lk(cond_variable_state.m); + cond_variable_state.ready = true; + std::cout << "[COND_VARIABLE] main() signals data ready " + "for processing" << std::endl; + } + cond_variable_state.cv.notify_one(); + + // wait for the worker + { + std::unique_lock lk(cond_variable_state.m); + cond_variable_state.cv.wait(lk, [&]{ return cond_variable_state.processed; }); + } + std::cout << "[COND_VARIABLE] Back in main(), data = " + << cond_variable_state.data << std::endl; + + if(cond_variable_state.data != "Example data after processing") + throw TestCaseException("[COND_VARIABLE]: Unexpected value for data!"); + + worker.join(); + + std::cout << "[COND_VARIABLE] Finished test." << std::endl; +} + +/* ===== TEST CASE 5: std::scoped_lock ===== + The parent thread creates 4 different employees, + each of which has a list of lunch_partners as well as a + mutex to control access to them. The main thread then + creates 4 child threads, passing each a different pair + of employees. Each child thread uses a std::scoped_lock + to acquire the two employee locks simultaneously, before + adding them to each other's lunch partner lists. Finally, + the resulting lunch partner lists are printed. +*/ +struct Employee { + std::vector<std::string> lunch_partners; + std::string id; + std::mutex m; + Employee(std::string id) : id(id) {} + std::string partners() const + { + std::string ret = "Employee " + id + " has lunch partners: "; + for (const auto& partner : lunch_partners) + ret += partner + " "; + return ret; + } +}; + +static void send_mail(Employee &, Employee &) { + // simulate a time-consuming messaging operation + std::this_thread::yield(); +} + +static void assign_lunch_partner(Employee &e1, Employee &e2) { + static thread_local std::mutex io_mutex; + { + std::lock_guard<std::mutex> lk(io_mutex); + std::cout << "[SCOPED_LOCK] " << e1.id << " and " << e2.id + << " are waiting for locks" << std::endl; + } + + { + // use std::scoped_lock to acquire two locks without worrying about + // other calls to assign_lunch_partner deadlocking us + // and it also provides a convenient RAII-style mechanism + + std::scoped_lock lock(e1.m, e2.m); + + // Equivalent code 1 (using std::lock and std::lock_guard) + // std::lock(e1.m, e2.m); + // std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock); + // std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock); + + // Equivalent code 2 (if unique_locks are needed, e.g. for condition variables) + // std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock); + // std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock); + // std::lock(lk1, lk2); + { + std::lock_guard<std::mutex> lk(io_mutex); + std::cout << "[SCOPED_LOCK] " << e1.id << " and " << e2.id + << " got locks" << std::endl; + } + e1.lunch_partners.push_back(e2.id); + e2.lunch_partners.push_back(e1.id); + } + + send_mail(e1, e2); + send_mail(e2, e1); +} + +static void test_scoped_lock() { + Employee ryo("RyoHazuki"), amigo("SambaDeAmigo"), + eggman("Dr.Eggman"), ulala("Ulala"); + + // assign in parallel threads because mailing users about lunch assignments + // takes a long time + std::vector<std::thread> threads; + threads.emplace_back(assign_lunch_partner, std::ref(ryo), std::ref(amigo)); + threads.emplace_back(assign_lunch_partner, std::ref(eggman), std::ref(amigo)); + threads.emplace_back(assign_lunch_partner, std::ref(eggman), std::ref(ryo)); + threads.emplace_back(assign_lunch_partner, std::ref(ulala), std::ref(amigo)); + + for (auto &thread : threads) + thread.join(); + + std::cout << "[SCOPED_LOCK] " << ryo.partners() << '\n' + << "[SCOPED_LOCK] " << amigo.partners() << '\n' + << "[SCOPED_LOCK] " << eggman.partners() << '\n' + << "[SCOPED_LOCK] " << ulala.partners() << std::endl; +} + +/* ===== TEST CASE 6: std::barrier ===== + The parent thread creates a list of 4 workers and a std::sync_point + of the same size with a completion callback. It then spawns a child ...<truncated>... hooks/post-receive -- A pseudo Operating System for the Dreamcast. |