From 3025c989dcd72a04d521c20f75a92f2a25136f74 Mon Sep 17 00:00:00 2001 From: nickpdemarco <nickpdemarco@gmail.com> Date: Tue, 30 May 2023 17:32:25 -0400 Subject: [PATCH 01/13] Contracts --- CMakeLists.txt | 13 +- rustport/.gitignore | 1 + rustport/CMakeLists.txt | 35 ++ rustport/Cargo.lock | 520 +++++++++++++++++++++++++ rustport/Cargo.toml | 14 + rustport/cppshim/include/bindings.hpp | 67 ++++ rustport/src/lib.rs | 26 ++ rustport/src/stlab.rs | 446 +++++++++++++++++++++ stlab/concurrency/default_executor.hpp | 9 + 9 files changed, 1130 insertions(+), 1 deletion(-) create mode 100644 rustport/.gitignore create mode 100644 rustport/CMakeLists.txt create mode 100644 rustport/Cargo.lock create mode 100644 rustport/Cargo.toml create mode 100644 rustport/cppshim/include/bindings.hpp create mode 100644 rustport/src/lib.rs create mode 100644 rustport/src/stlab.rs diff --git a/CMakeLists.txt b/CMakeLists.txt index fca77b6a..90a97738 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,8 @@ set(STLAB_TASK_SYSTEM ${STLAB_DEFAULT_TASK_SYSTEM} CACHE STRING "Task system to stlab_detect_main_executor(STLAB_DEFAULT_MAIN_EXECUTOR) set(STLAB_MAIN_EXECUTOR ${STLAB_DEFAULT_MAIN_EXECUTOR} CACHE STRING "Main executor to use (qt5|qt6|libdispatch|emscripten|none).") +option( STLAB_USE_RUST_DEFAULT_EXECUTOR "Use a Rust port of the default_executor. Defaults to OFF." ON ) + if( BUILD_TESTING AND NOT Boost_unit_test_framework_FOUND ) message( SEND_ERROR "BUILD_TESTING is enabled, but an installation of Boost.Test was not found." ) endif() @@ -109,6 +111,8 @@ elseif (STLAB_MAIN_EXECUTOR STREQUAL "qt6") target_link_libraries( stlab INTERFACE Qt6::Core ) endif() + + message(STATUS "stlab: Use Boost C++17 Shims: ${STLAB_USE_BOOST_CPP17_SHIMS}") message(STATUS "stlab: Disable Coroutines: ${STLAB_DEFAULT_NO_STD_COROUTINES}") message(STATUS "stlab: Thread System: ${STLAB_THREAD_SYSTEM}") @@ -124,7 +128,7 @@ if ( BUILD_TESTING ) # add_library( testing INTERFACE ) add_library( stlab::testing ALIAS testing ) - + # # CMake targets linking to the stlab::testing target will (transitively) # link to the Boost::unit_test_framework and to stlab::stlab target. @@ -134,6 +138,13 @@ if ( BUILD_TESTING ) stlab::development stlab::stlab ) + if ( STLAB_USE_RUST_DEFAULT_EXECUTOR ) + add_definitions( -DSTLAB_USE_RUST_DEFAULT_EXECUTOR ) + add_subdirectory( rustport ) + include_directories( rustport ) + target_link_libraries( testing INTERFACE RustyDefaultExecutor ) + endif() + # # Linking to the Boost unit test framework requires an additional # preprocessor definition when the unit test compiled resources are diff --git a/rustport/.gitignore b/rustport/.gitignore new file mode 100644 index 00000000..eb5a316c --- /dev/null +++ b/rustport/.gitignore @@ -0,0 +1 @@ +target diff --git a/rustport/CMakeLists.txt b/rustport/CMakeLists.txt new file mode 100644 index 00000000..361d91c4 --- /dev/null +++ b/rustport/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.25) + +include(FetchContent) + +project("RustyDefaultExecutor") + +FetchContent_Declare( + Corrosion + GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git + GIT_TAG master # Needed for experimental feature `corrosion_experimental_cbindgen`. +) + +# Set any global configuration variables such as `Rust_TOOLCHAIN` before this line! + +FetchContent_MakeAvailable(Corrosion) + +corrosion_import_crate(MANIFEST_PATH ./Cargo.toml) + +corrosion_experimental_cbindgen( + TARGET default_executor + HEADER_NAME "bindings.h" +) + +add_library(${PROJECT_NAME} INTERFACE) + +target_include_directories(${PROJECT_NAME} + # PRIVATE + # # where the library itself will look for its internal headers + # ${CMAKE_CURRENT_SOURCE_DIR}/src + INTERFACE + # where top-level project will look for the library's public headers + $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/cppshim/include> +) + +target_link_libraries(${PROJECT_NAME} INTERFACE default_executor) diff --git a/rustport/Cargo.lock b/rustport/Cargo.lock new file mode 100644 index 00000000..2a4be3dd --- /dev/null +++ b/rustport/Cargo.lock @@ -0,0 +1,520 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cbindgen" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6358dedf60f4d9b8db43ad187391afe959746101346fe51bb978126bec61dfb" +dependencies = [ + "clap", + "heck", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", + "tempfile", + "toml", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef2b3ded6a26dfaec672a742c93c8cf6b689220324da509ec5caa20de55dc83" +dependencies = [ + "atty", + "bitflags", + "clap_lex", + "indexmap", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "default_executor" +version = "0.1.0" +dependencies = [ + "cbindgen", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "libc" +version = "0.2.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" + +[[package]] +name = "linux-raw-sys" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "os_str_bytes" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" + +[[package]] +name = "proc-macro2" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustix" +version = "0.37.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "serde" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.45.0", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/rustport/Cargo.toml b/rustport/Cargo.toml new file mode 100644 index 00000000..a58c1de4 --- /dev/null +++ b/rustport/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "default_executor" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[build-dependencies] +cbindgen = "0.24.0" diff --git a/rustport/cppshim/include/bindings.hpp b/rustport/cppshim/include/bindings.hpp new file mode 100644 index 00000000..a5a0cd0b --- /dev/null +++ b/rustport/cppshim/include/bindings.hpp @@ -0,0 +1,67 @@ +#include "bindings.h" + +#include <utility> +#include <iostream> + +namespace stlab { +inline namespace v1 { +namespace detail { + +// REVISIT - have to invert the priority encoding to match old API. +enum class executor_priority { high = 2, medium = 1, low = 0 }; + +/// @brief Invokes `f` on the default_executor. +/// @tparam F function object type +/// @param f a function object with signature `void()`. +/// @return the value returned by `execute`. +template <class F> +auto enqueue(F f) { + using f_t = decltype(f); + return execute(new f_t(std::move(f)), [](void* f_) { + auto f = static_cast<f_t*>(f_); + (*f)(); + delete f; + }); +} + +/// @brief Invokes `f` on the default_executor at the given `priority`. +/// @tparam F function object type +/// @param f a function object with signature `void()`. +/// @param priority +/// @return the value returned by `execute_priority`. +template <class F> +auto enqueue_priority(F f, executor_priority priority) { + using f_t = decltype(f); + return execute_priority(new f_t(std::move(f)), [](void* f_) { + auto f = static_cast<f_t*>(f_); + (*f)(); + delete f; + }, static_cast<std::size_t>(priority)); +} + +/// @brief A thin invokable wrapper around `enqueue_priority`. +/// @tparam Priority the priority at which all given function objects will be enqueued. +template <executor_priority Priority> +struct executor_type { + using result_type = void; + + /// @brief Enqueues the given task on the default_executor with this object's Priority value. + /// @tparam F function object type + /// @param f function object + template <class F> + void operator()(F&& f) const { + enqueue_priority(std::forward<F>(f), Priority); + } +}; + +} // namespace detail + +/// @brief An executor for low priority tasks, enqueued with the call operator. +constexpr auto low_executor = detail::executor_type<detail::executor_priority::low>{}; +/// @brief An executor for standard priority tasks, enqueued with the call operator. +constexpr auto default_executor = detail::executor_type<detail::executor_priority::medium>{}; +/// @brief An executor for high priority tasks, enqueued with the call operator. +constexpr auto high_executor = detail::executor_type<detail::executor_priority::high>{}; + +} // inline namespace v1 +} // namespace stlab diff --git a/rustport/src/lib.rs b/rustport/src/lib.rs new file mode 100644 index 00000000..95f69f04 --- /dev/null +++ b/rustport/src/lib.rs @@ -0,0 +1,26 @@ +use std::ffi::c_void; + +mod stlab; + +/// Enqueues a the execution of `f(context)` on the PriorityTaskSystem. +#[no_mangle] +pub extern "C" fn execute(context: *mut c_void, f: extern fn(*mut c_void)) -> i32 { + stlab::PriorityTaskSystem::singleton().execute(move||{ + f(context) + }, stlab::Priority(0)); + 0 +} + +/// Enqueues a the execution of `f(context)` on the PriorityTaskSystem at the given `priority`. +#[no_mangle] +pub extern "C" fn execute_priority(context: *mut c_void, f: extern fn(*mut c_void), priority: usize) -> i32 { + stlab::PriorityTaskSystem::singleton().execute(move||{ + f(context) + }, stlab::Priority(priority)); + 0 +} + +#[cfg(test)] +mod tests { + use super::*; +} diff --git a/rustport/src/stlab.rs b/rustport/src/stlab.rs new file mode 100644 index 00000000..a498cd44 --- /dev/null +++ b/rustport/src/stlab.rs @@ -0,0 +1,446 @@ +use std::cmp::{Ordering, max, Eq, Ord, PartialEq, PartialOrd}; +use std::mem::MaybeUninit; +use std::num::NonZeroUsize; +use std::sync::{Mutex, Once}; +use std::sync::atomic::{AtomicUsize, Ordering as MemoryOrdering}; + +/// A type-erased, heap-allocated function object. +type Task = Box<dyn FnOnce()->()>; + +/// A `usize` constraining valid values to [0, 4) with runtime assertions. +#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] +pub struct Priority(pub usize); + +impl Priority { + pub fn new(value: usize) -> Self { + assert!((0..4).contains(&value), "Priorities must be in [0, 4)"); + Self(value) + } + + /// Returns a usize of the form 0bXX000000 where XX is a binary representation of this priority. + /// The value of this priority is guaranteed to fit in two bits because `Priority` values are constrained to [0b00, 0b11]. + pub fn to_highbit_mask(&self) -> usize { + &self.0 << (usize::BITS - 2) + } +} + +/// Pairs an instance of `T` with a `Priority`. +/// Equality and ordering of a Prioritized<T> only considers `priority`, disregarding `element`. +struct Prioritized <T> { + priority: Priority, + element: T +} + +impl<T> PartialEq for Prioritized <T> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.priority == other.priority + } +} + +impl <T> Eq for Prioritized <T> {} + +impl<T> PartialOrd for Prioritized <T> { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + self.priority.partial_cmp(&other.priority) + } +} + +impl<T> Ord for Prioritized <T> { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.priority.cmp(&other.priority) + } +} + +/// The fields of `Waiter` which must be protected by a `Mutex`. +struct WaiterProtectedData { + waiting: bool, + done: bool, +} + +/// A utility for suspending a thread using a condition variable. +struct Waiter { + protected: Mutex<WaiterProtectedData>, + ready: std::sync::Condvar, +} + +impl Waiter { + + /// Constructs a new Waiter, with `waiting` and `done` set to `false`. + pub fn new() -> Self { + Self { + protected: Mutex::new(WaiterProtectedData { + waiting: false, + done: false, + }), + ready: std::sync::Condvar::new() + } + } + + /// Sets `done` to `true`, and notifies one waiter of our condition variable. + pub fn done(&self) { + { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.done = true; + } + self.ready.notify_one(); + } + + /// Sets waiting to `false`. If waiting was `true`, wake one waiter and return `true`. Otherwise, return `false`. + /// If `try_lock` fails, return `false`. (REVIEW: why?) + /// (REVIEW: is it redundant to express that `waiting` and `done` are accesed under a mutex?) + pub fn wake(&self) -> bool { + if let Ok(ref mut this) = self.protected.try_lock() { + if !this.waiting { + return false; + } + this.waiting = false; + } else { + return false; + } + self.ready.notify_one(); + return true; + } + + /// Block this thread until `wake()` or `done()` is called. + /// Returns `true` if `done()` has been called, otherwise `false`. + pub fn wait(&self) -> bool { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.waiting = true; + while this.waiting && !this.done { + this = self.ready.wait(this).expect("the mutex is not poisoned"); + } + this.waiting = false; + return this.done; + } +} + +/// The fields of `NotificationQueue` which must be protected by a `Mutex`. +struct NotificationQueueProtectedData <T> { + heap: std::collections::BinaryHeap<Prioritized<T>>, + count: usize, + done: bool, + waiting: bool, +} + +impl<T> std::default::Default for NotificationQueueProtectedData<T> { + fn default() -> Self { + Self { + heap: std::collections::BinaryHeap::new(), + count: 0, + done: false, + waiting: false + } + } +} + +/// A threadsafe priority queue. +struct NotificationQueue<T> { + // In the C++ implementation, we use a single lock for multiple data fields. + // In Rust, we require exactly one mutex per protected field. + // So, put protected fields into a separate struct, and lock on that. + // Note the transformation to use multiple locks is non-trivial, because + // this would require the ordering of acquired locks to be identical in all + // code paths to prevent deadlock. + protected: Mutex<NotificationQueueProtectedData<T>>, + ready: std::sync::Condvar, +} + +impl<T> std::default::Default for NotificationQueue<T> { + fn default() -> Self { + Self { + protected: Mutex::new(NotificationQueueProtectedData::<T>::default()), + ready: std::sync::Condvar::new(), + } + } +} + +impl<T> NotificationQueue<T> { + + /// Merge priority and count into a single usize, storing the former in the + /// two highest bits of the result. This requires priority be in [0, 4) i.e., + /// takes up two bits. + fn merge_priority_count(priority: Priority, count: usize) -> Priority { + Priority(priority.to_highbit_mask() | count) + } + + /// Try to pop from the queue without blocking. + /// Returns `None` if our mutex is already locked or if the queue is empty. + pub fn try_pop(&mut self) -> Option<T> { + if let Ok(ref mut this) = self.protected.try_lock() { + if !this.heap.is_empty() { + return Some(this.heap.pop().unwrap().element); + } + } + return None; + } + + /// If waiting in `pop()`, wakes and returns true. Otherwise returns false. + pub fn wake(&self) -> bool { + if let Ok(ref mut this) = self.protected.try_lock() { + if !this.waiting { + return false; + } + this.waiting = false; // triggers wake + } + return false; + } + + /// Pop from the queue, suspending the current thread until an element is available. + /// The returned `bool` indicates if this object is `done()`. + pub fn pop(&self) -> (bool, Option<T>) { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.waiting = true; + while !this.heap.is_empty() && !this.done && this.waiting { + this = self.ready.wait(this).expect("the mutex is not poisoned"); + } + this.waiting = false; + if this.heap.is_empty() { + return (this.done, None); + } + return (false, Some(this.heap.pop().unwrap().element)); + } + + /// Mark this object for teardown, and wake any thread awaiting an available element in `pop()`. + pub fn done(&self) { + { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.done = true + } + self.ready.notify_one(); + } + + /// Try to push `element` to the queue without blocking, returning `element` if our mutex is already locked. + /// If the push succeeds, wake a thread which may be awaiting an element in `pop()`. + pub fn try_push(&self, element: T, priority: Priority) -> Option<T> { + if let Ok(ref mut this) = self.protected.try_lock() { + let priority = Self::merge_priority_count(priority, this.count); + this.count += 1; + this.heap.push(Prioritized{ element, priority }); + } else { + return Some(element); + } + + // We successfully locked the mutex, did our push, and released the lock. + self.ready.notify_one(); + return None; + } + + /// Push `element` to the queue, blocking if our mutex is already locked. + /// When the push succeeds, wake a thread which may be awaiting an element in `pop()`. + pub fn push(&self, element: T, priority: Priority) { + { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + let priority = Self::merge_priority_count(priority, this.count); + this.count += 1; + this.heap.push(Prioritized{ element, priority }); + } + self.ready.notify_one(); + } +} + +/// The fields of `PriorityTaskSystem` which must be protected by a `Mutex`. +#[derive(Default)] +struct PriorityTaskSystemProtectedData { + threads: Vec<std::thread::JoinHandle<()>>, + waiters: Vec<Waiter>, +} + +impl PriorityTaskSystemProtectedData { + fn spawn_thread<F>(thread_name: &'static str, f: F) -> std::thread::JoinHandle<()> + where F: FnOnce() -> () + 'static + Send { + std::thread::Builder::new().name(thread_name.to_string()) + .spawn(f).expect("spawning a thread does not fail") + } + + /// Spawn `thread_count` threads, each of which pops tasks from an associated queue in `queues`, + /// or steals tasks from other queues if no task is available. + pub(self) fn spawn_baseline_threads(&mut self, thread_count: usize, available_parallelism: usize) { + self.threads.extend((0..thread_count).map(|i| { + Self::spawn_thread("cc.stlab.default_executor", move || { + loop { + let this = PriorityTaskSystem::singleton(); // why doesn't this have to be mut? + let mut task: Option<Task> = None; + for n in 0..available_parallelism { + match task { + Some(_) => { break } + _ => { + task = this.queues[(i + n) % available_parallelism].try_pop() + } + } + } + + if task.is_none() { + let done: bool; + (done, task) = this.queues[i].pop(); + if done { break } + } + + if task.is_some() { + task.unwrap()(); + } + } + }) + })) + } + + /// Spawn one thread which will repeatedly check each queue in `queues` for tasks, and execute them. + /// If no tasks are found, the thread is suspended. + pub(self) fn spawn_expansion_thread(&mut self, available_parallelism: usize) { + let protected = PriorityTaskSystem::singleton().protected.get_mut().expect("the mutex is not poisoned"); + let i = protected.threads.len(); + + if i == PriorityTaskSystem::singleton().available_parallelism { + eprintln!("Thread limit reached") + } + + self.threads.push(Self::spawn_thread("cc.stlab.default_executor.expansion", move || { + loop { + let this = PriorityTaskSystem::singleton(); // why doesn't this have to be mut? + let mut task: Option<Task> = None; + for n in 0..available_parallelism { + match task { + Some(_) => { break } + _ => { + task = this.queues[(i + n) % available_parallelism].try_pop() + } + } + } + + if task.is_some() { + task.unwrap()(); + continue; + } + + let protected = this.protected.get_mut().expect("the mutex is not poisoned"); + + // Note: The following means multiple threads may wait on a single `Waiter`. + if protected.waiters[i - available_parallelism].wait() { break; } + } + })); + } +} + +/// A thread-scalable priority task system. +pub struct PriorityTaskSystem { + available_parallelism: usize, // _count in C++ implementation. + thread_limit: usize, + queues: Vec<NotificationQueue<Task>>, + index: AtomicUsize, + protected: Mutex<PriorityTaskSystemProtectedData> +} + +impl PriorityTaskSystem { + + // TODO: Roll back the singleton pattern, and find a way to make instances immovable. + /// Return the singleton instance of `PriorityTaskSystem`, creating it on the first invocation. + pub fn singleton() -> &'static mut Self { + static mut INSTANCE: MaybeUninit<PriorityTaskSystem> = MaybeUninit::uninit(); + static ONCE: Once = Once::new(); + + ONCE.call_once(|| { + // This clunky expression is to convert from a NonZeroUsize to a usize for the subsequent arithmetic. I'd like a better way. + // SAFETY: we know 1 is not 0. + let nonzero_available_parallelism = std::thread::available_parallelism().unwrap_or(unsafe { NonZeroUsize::new_unchecked(1) }); + let available_parallelism = max(usize::from(nonzero_available_parallelism), 2) - 1; + let thread_limit = max(9, available_parallelism * 4 + 1); + + unsafe { + INSTANCE.as_mut_ptr().write(Self { + available_parallelism, + thread_limit, + queues: { + let mut v = Vec::with_capacity(available_parallelism); + for _ in 0..available_parallelism { v.push(NotificationQueue::<Task>::default()) } + v + }, + index: AtomicUsize::new(0), + protected: Mutex::new(PriorityTaskSystemProtectedData { + waiters: Vec::with_capacity(thread_limit - available_parallelism), + threads: Vec::with_capacity(available_parallelism), + }) + }); + } + + // Defer spawning threads so they can never witness an uninitialized `INSTANCE`. + // Note `get_mut` statically enforces exclusive access; no locking will take place. + unsafe { + // `unwrap` should never panic; that would imply the mutex we just created, and never + // handed out, has been poisoned. + (*INSTANCE.as_mut_ptr()).protected.get_mut().unwrap().spawn_baseline_threads(available_parallelism, available_parallelism); + } + }); + + unsafe { &mut *INSTANCE.as_mut_ptr() } + } + + /// Mark all subobjects for teardown, and join all threads spawned by this object. + pub fn join(&mut self) { + for queue in &self.queues { + queue.done(); + } + + // Consume the contents under the mutex, leaving them empty. + let this = std::mem::take(self.protected.get_mut().expect("the mutex is not poisoned")); + for e in this.waiters { + e.done(); + } + for e in this.threads { + let _ = e.join(); + } + + self.queues.clear(); + } + + /// Push `f` to the first queue in `queues` whose mutex is not under contention. + /// If no such queue is found after a single pass, blockingly push `f` to one queue. + // REVIEW: I'm not sure `execute` is a good name. I think we want `push`, or `push_with_priority`. + pub fn execute<F>(&mut self, f: F, p: Priority) where F: FnOnce() -> () + 'static { + let mut task: Option<Task> = Some(Box::new(f)); + // Use SeqCst to match the default for C++'s std::atomic<unsigned>::operator++(int). + let i = self.index.fetch_add(1, MemoryOrdering::SeqCst); + for n in 0..i { + task = self.queues[(i + n) % self.available_parallelism].try_push(task.unwrap(), p); + if task.is_none() { return } + } + + self.queues[i % self.available_parallelism].push(task.unwrap(), p); + } + + /// Spawn an expansion thread which will steal tasks from `queues`. + pub fn add_thread(&mut self) { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + if this.threads.len() == self.thread_limit { + eprintln!("Unable to add thread; thread_limit reached: {}", self.thread_limit); + return; + } + + this.spawn_expansion_thread(self.available_parallelism); + } + + /// Get the number of waiters. + fn waiters_size(&self) -> usize { + let this = self.protected.lock().expect("the mutex is not poisoned"); + this.threads.len() - self.available_parallelism + } + + /// Wake every `Waiter` in `waiters`. Return true if any indicate they are `done()`, otherwise `false`. + pub fn wake(&mut self) -> bool { + for queue in &self.queues { + if queue.wake() { return true } + } + + let size = self.waiters_size(); + // Note: get_mut ensures no locking occurs; uniqueness enforced by the type system. + let this = self.protected.get_mut().expect("the mutex is not poisoned"); + for n in 0..size { + if this.waiters[n].wake() { + return true; + } + } + + return false; + } +} diff --git a/stlab/concurrency/default_executor.hpp b/stlab/concurrency/default_executor.hpp index a4a966fd..a4831f9c 100644 --- a/stlab/concurrency/default_executor.hpp +++ b/stlab/concurrency/default_executor.hpp @@ -9,6 +9,12 @@ #ifndef STLAB_CONCURRENCY_DEFAULT_EXECUTOR_HPP #define STLAB_CONCURRENCY_DEFAULT_EXECUTOR_HPP +#ifdef STLAB_USE_RUST_DEFAULT_EXECUTOR + +#include "bindings.hpp" + +#else + #include <stlab/concurrency/set_current_thread_name.hpp> #include <stlab/concurrency/task.hpp> #include <stlab/config.hpp> @@ -490,6 +496,7 @@ constexpr auto low_executor = detail::executor_type<detail::executor_priority::l constexpr auto default_executor = detail::executor_type<detail::executor_priority::medium>{}; constexpr auto high_executor = detail::executor_type<detail::executor_priority::high>{}; + /**************************************************************************************************/ } // namespace v1 @@ -500,6 +507,8 @@ constexpr auto high_executor = detail::executor_type<detail::executor_priority:: /**************************************************************************************************/ +#endif // STLAB_USE_RUST_DEFAULT_EXECUTOR + #endif // STLAB_CONCURRENCY_DEFAULT_EXECUTOR_HPP /**************************************************************************************************/ From 3a964fc734d262dc3803b0cbe666a9c553eede0b Mon Sep 17 00:00:00 2001 From: nickpdemarco <nickpdemarco@gmail.com> Date: Tue, 20 Jun 2023 15:06:22 -0400 Subject: [PATCH 02/13] STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) --- CMakeLists.txt | 18 +++++++++--------- cmake/StlabUtil.cmake | 2 ++ stlab/concurrency/default_executor.hpp | 7 ++++--- stlab/concurrency/system_timer.hpp | 6 +++--- stlab/config.hpp.in | 1 + 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 90a97738..2da42ac7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,13 +26,11 @@ stlab_detect_thread_system(STLAB_DEFAULT_THREAD_SYSTEM) set( STLAB_THREAD_SYSTEM ${STLAB_DEFAULT_THREAD_SYSTEM} CACHE STRING "Thread system to use (win32|pthread|pthread-emscripten|pthread-apple|none)") stlab_detect_task_system(STLAB_DEFAULT_TASK_SYSTEM) -set(STLAB_TASK_SYSTEM ${STLAB_DEFAULT_TASK_SYSTEM} CACHE STRING "Task system to use (portable|libdispatch|windows).") +set(STLAB_TASK_SYSTEM ${STLAB_DEFAULT_TASK_SYSTEM} CACHE STRING "Task system to use (portable|libdispatch|windows|experimental_rust).") stlab_detect_main_executor(STLAB_DEFAULT_MAIN_EXECUTOR) set(STLAB_MAIN_EXECUTOR ${STLAB_DEFAULT_MAIN_EXECUTOR} CACHE STRING "Main executor to use (qt5|qt6|libdispatch|emscripten|none).") -option( STLAB_USE_RUST_DEFAULT_EXECUTOR "Use a Rust port of the default_executor. Defaults to OFF." ON ) - if( BUILD_TESTING AND NOT Boost_unit_test_framework_FOUND ) message( SEND_ERROR "BUILD_TESTING is enabled, but an installation of Boost.Test was not found." ) endif() @@ -103,6 +101,11 @@ if (STLAB_TASK_SYSTEM STREQUAL "libdispatch") target_link_libraries(stlab INTERFACE libdispatch::libdispatch) endif() +if (STLAB_TASK_SYSTEM STREQUAL "experimental_rust") + add_subdirectory( rustport ) + include_directories( rustport ) +endif() + if (STLAB_MAIN_EXECUTOR STREQUAL "libdispatch") target_link_libraries(stlab INTERFACE libdispatch::libdispatch) elseif (STLAB_MAIN_EXECUTOR STREQUAL "qt5") @@ -138,12 +141,9 @@ if ( BUILD_TESTING ) stlab::development stlab::stlab ) - if ( STLAB_USE_RUST_DEFAULT_EXECUTOR ) - add_definitions( -DSTLAB_USE_RUST_DEFAULT_EXECUTOR ) - add_subdirectory( rustport ) - include_directories( rustport ) - target_link_libraries( testing INTERFACE RustyDefaultExecutor ) - endif() + if (STLAB_TASK_SYSTEM STREQUAL "experimental_rust") + target_link_libraries( testing INTERFACE RustyDefaultExecutor ) + endif () # # Linking to the Boost unit test framework requires an additional diff --git a/cmake/StlabUtil.cmake b/cmake/StlabUtil.cmake index b5bba2da..28ff7f0b 100644 --- a/cmake/StlabUtil.cmake +++ b/cmake/StlabUtil.cmake @@ -190,6 +190,8 @@ function( stlab_generate_config_file ) set( STLAB_TASK_SYSTEM_EMSCRIPTEN TRUE ) elseif (STLAB_TASK_SYSTEM STREQUAL "windows") set( STLAB_TASK_SYSTEM_WINDOWS TRUE ) + elseif (STLAB_TASK_SYSTEM STREQUAL "experimental_rust") + set( STLAB_TASK_SYSTEM_EXPERIMENTAL_RUST TRUE ) endif() if (STLAB_MAIN_EXECUTOR STREQUAL "libdispatch") diff --git a/stlab/concurrency/default_executor.hpp b/stlab/concurrency/default_executor.hpp index a4831f9c..98bb0e13 100644 --- a/stlab/concurrency/default_executor.hpp +++ b/stlab/concurrency/default_executor.hpp @@ -9,7 +9,9 @@ #ifndef STLAB_CONCURRENCY_DEFAULT_EXECUTOR_HPP #define STLAB_CONCURRENCY_DEFAULT_EXECUTOR_HPP -#ifdef STLAB_USE_RUST_DEFAULT_EXECUTOR +#include <stlab/config.hpp> + +#if STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) #include "bindings.hpp" @@ -17,7 +19,6 @@ #include <stlab/concurrency/set_current_thread_name.hpp> #include <stlab/concurrency/task.hpp> -#include <stlab/config.hpp> #include <stlab/pre_exit.hpp> #include <cassert> @@ -507,7 +508,7 @@ constexpr auto high_executor = detail::executor_type<detail::executor_priority:: /**************************************************************************************************/ -#endif // STLAB_USE_RUST_DEFAULT_EXECUTOR +#endif // STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) #endif // STLAB_CONCURRENCY_DEFAULT_EXECUTOR_HPP diff --git a/stlab/concurrency/system_timer.hpp b/stlab/concurrency/system_timer.hpp index 6ac84f37..91dc433a 100755 --- a/stlab/concurrency/system_timer.hpp +++ b/stlab/concurrency/system_timer.hpp @@ -22,7 +22,7 @@ #elif STLAB_TASK_SYSTEM(WINDOWS) #include <Windows.h> #include <memory> -#elif STLAB_TASK_SYSTEM(PORTABLE) +#elif STLAB_TASK_SYSTEM(PORTABLE) || STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) #include <algorithm> #include <condition_variable> @@ -167,7 +167,7 @@ class system_timer { /**************************************************************************************************/ -#elif STLAB_TASK_SYSTEM(PORTABLE) +#elif STLAB_TASK_SYSTEM(PORTABLE) || STLAB_TASK_SYSTEM(PORTABLE_RUST) class system_timer { using element_t = std::pair<std::chrono::steady_clock::time_point, task<void()>>; @@ -246,7 +246,7 @@ class system_timer { /**************************************************************************************************/ -#if STLAB_TASK_SYSTEM(WINDOWS) || STLAB_TASK_SYSTEM(PORTABLE) +#if STLAB_TASK_SYSTEM(WINDOWS) || STLAB_TASK_SYSTEM(PORTABLE) || STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) struct system_timer_type { using result_type = void; diff --git a/stlab/config.hpp.in b/stlab/config.hpp.in index 953093ee..5c3cf98e 100644 --- a/stlab/config.hpp.in +++ b/stlab/config.hpp.in @@ -24,6 +24,7 @@ #cmakedefine01 STLAB_TASK_SYSTEM_LIBDISPATCH() #cmakedefine01 STLAB_TASK_SYSTEM_EMSCRIPTEN() #cmakedefine01 STLAB_TASK_SYSTEM_WINDOWS() +#cmakedefine01 STLAB_TASK_SYSTEM_EXPERIMENTAL_RUST() #define STLAB_MAIN_EXECUTOR(X) (STLAB_MAIN_EXECUTOR_##X()) #cmakedefine01 STLAB_MAIN_EXECUTOR_LIBDISPATCH() From 2cf0ee781dfb4ff23630c70908c3d575995efa5c Mon Sep 17 00:00:00 2001 From: nickpdemarco <nickpdemarco@gmail.com> Date: Tue, 20 Jun 2023 15:41:35 -0400 Subject: [PATCH 03/13] Responding to feedback --- CMakeLists.txt | 1 - rustport/CMakeLists.txt | 8 +++--- rustport/cppshim/include/bindings.hpp | 39 ++++++++++++++------------- stlab/concurrency/system_timer.hpp | 2 +- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2da42ac7..9a9e3001 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,7 +103,6 @@ endif() if (STLAB_TASK_SYSTEM STREQUAL "experimental_rust") add_subdirectory( rustport ) - include_directories( rustport ) endif() if (STLAB_MAIN_EXECUTOR STREQUAL "libdispatch") diff --git a/rustport/CMakeLists.txt b/rustport/CMakeLists.txt index 361d91c4..cba82edc 100644 --- a/rustport/CMakeLists.txt +++ b/rustport/CMakeLists.txt @@ -2,8 +2,6 @@ cmake_minimum_required(VERSION 3.25) include(FetchContent) -project("RustyDefaultExecutor") - FetchContent_Declare( Corrosion GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git @@ -21,9 +19,9 @@ corrosion_experimental_cbindgen( HEADER_NAME "bindings.h" ) -add_library(${PROJECT_NAME} INTERFACE) +add_library(RustyDefaultExecutor INTERFACE) -target_include_directories(${PROJECT_NAME} +target_include_directories(RustyDefaultExecutor # PRIVATE # # where the library itself will look for its internal headers # ${CMAKE_CURRENT_SOURCE_DIR}/src @@ -32,4 +30,4 @@ target_include_directories(${PROJECT_NAME} $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/cppshim/include> ) -target_link_libraries(${PROJECT_NAME} INTERFACE default_executor) +target_link_libraries(RustyDefaultExecutor INTERFACE default_executor) diff --git a/rustport/cppshim/include/bindings.hpp b/rustport/cppshim/include/bindings.hpp index a5a0cd0b..03407ae1 100644 --- a/rustport/cppshim/include/bindings.hpp +++ b/rustport/cppshim/include/bindings.hpp @@ -1,7 +1,8 @@ #include "bindings.h" -#include <utility> #include <iostream> +#include <type_traits> +#include <utility> namespace stlab { inline namespace v1 { @@ -10,33 +11,35 @@ namespace detail { // REVISIT - have to invert the priority encoding to match old API. enum class executor_priority { high = 2, medium = 1, low = 0 }; -/// @brief Invokes `f` on the default_executor. +/// @brief Asynchronously invoke f on the default_executor. /// @tparam F function object type -/// @param f a function object with signature `void()`. -/// @return the value returned by `execute`. -template <class F> +/// @param f a function object. +/// @return the result of calling `execute`. +template <class F, typename = std::enable_if_t<std::is_invocable_r<void, F>::value>> auto enqueue(F f) { - using f_t = decltype(f); - return execute(new f_t(std::move(f)), [](void* f_) { - auto f = static_cast<f_t*>(f_); - (*f)(); + return execute(new F(std::move(f)), [](void* f_) { + auto f = static_cast<F*>(f_); + try { + (*f)(); + } catch (...) {} delete f; }); } -/// @brief Invokes `f` on the default_executor at the given `priority`. +/// @brief Asynchronously invoke `f` on the default_executor with priority `p`. /// @tparam F function object type -/// @param f a function object with signature `void()`. +/// @param f a function object. /// @param priority /// @return the value returned by `execute_priority`. -template <class F> -auto enqueue_priority(F f, executor_priority priority) { - using f_t = decltype(f); - return execute_priority(new f_t(std::move(f)), [](void* f_) { - auto f = static_cast<f_t*>(f_); - (*f)(); +template <class F, class = std::enable_if_t<std::is_invocable_r< void, F >::value> > +auto enqueue_priority(F f, executor_priority p) { + return execute_priority(new F(std::move(f)), [](void* f_) { + auto f = static_cast<F*>(f_); + try { + (*f)(); + } catch (...) {} delete f; - }, static_cast<std::size_t>(priority)); + }, static_cast<std::uint64_t>(p)); } /// @brief A thin invokable wrapper around `enqueue_priority`. diff --git a/stlab/concurrency/system_timer.hpp b/stlab/concurrency/system_timer.hpp index 91dc433a..eb58aeb6 100755 --- a/stlab/concurrency/system_timer.hpp +++ b/stlab/concurrency/system_timer.hpp @@ -167,7 +167,7 @@ class system_timer { /**************************************************************************************************/ -#elif STLAB_TASK_SYSTEM(PORTABLE) || STLAB_TASK_SYSTEM(PORTABLE_RUST) +#elif STLAB_TASK_SYSTEM(PORTABLE) || STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) class system_timer { using element_t = std::pair<std::chrono::steady_clock::time_point, task<void()>>; From 1518caf87a8b167bb80e5af44a3dd598d4305b6a Mon Sep 17 00:00:00 2001 From: nickpdemarco <nickpdemarco@gmail.com> Date: Thu, 12 Oct 2023 15:22:43 -0400 Subject: [PATCH 04/13] Break prioritized out into its own file --- rustport/src/{stlab.rs => stlab/mod.rs} | 60 ++++--------------------- rustport/src/stlab/prioritized.rs | 58 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 52 deletions(-) rename rustport/src/{stlab.rs => stlab/mod.rs} (90%) create mode 100644 rustport/src/stlab/prioritized.rs diff --git a/rustport/src/stlab.rs b/rustport/src/stlab/mod.rs similarity index 90% rename from rustport/src/stlab.rs rename to rustport/src/stlab/mod.rs index a498cd44..a98979c7 100644 --- a/rustport/src/stlab.rs +++ b/rustport/src/stlab/mod.rs @@ -1,59 +1,15 @@ -use std::cmp::{Ordering, max, Eq, Ord, PartialEq, PartialOrd}; +use std::cmp::max; use std::mem::MaybeUninit; use std::num::NonZeroUsize; use std::sync::{Mutex, Once}; use std::sync::atomic::{AtomicUsize, Ordering as MemoryOrdering}; +mod prioritized; +pub use prioritized::{Priority, Prioritized}; + /// A type-erased, heap-allocated function object. type Task = Box<dyn FnOnce()->()>; -/// A `usize` constraining valid values to [0, 4) with runtime assertions. -#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] -pub struct Priority(pub usize); - -impl Priority { - pub fn new(value: usize) -> Self { - assert!((0..4).contains(&value), "Priorities must be in [0, 4)"); - Self(value) - } - - /// Returns a usize of the form 0bXX000000 where XX is a binary representation of this priority. - /// The value of this priority is guaranteed to fit in two bits because `Priority` values are constrained to [0b00, 0b11]. - pub fn to_highbit_mask(&self) -> usize { - &self.0 << (usize::BITS - 2) - } -} - -/// Pairs an instance of `T` with a `Priority`. -/// Equality and ordering of a Prioritized<T> only considers `priority`, disregarding `element`. -struct Prioritized <T> { - priority: Priority, - element: T -} - -impl<T> PartialEq for Prioritized <T> { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.priority == other.priority - } -} - -impl <T> Eq for Prioritized <T> {} - -impl<T> PartialOrd for Prioritized <T> { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - self.priority.partial_cmp(&other.priority) - } -} - -impl<T> Ord for Prioritized <T> { - #[inline] - fn cmp(&self, other: &Self) -> Ordering { - self.priority.cmp(&other.priority) - } -} - /// The fields of `Waiter` which must be protected by a `Mutex`. struct WaiterProtectedData { waiting: bool, @@ -171,7 +127,7 @@ impl<T> NotificationQueue<T> { pub fn try_pop(&mut self) -> Option<T> { if let Ok(ref mut this) = self.protected.try_lock() { if !this.heap.is_empty() { - return Some(this.heap.pop().unwrap().element); + return Some(this.heap.pop().unwrap().take_element()); } } return None; @@ -200,7 +156,7 @@ impl<T> NotificationQueue<T> { if this.heap.is_empty() { return (this.done, None); } - return (false, Some(this.heap.pop().unwrap().element)); + return (false, Some(this.heap.pop().unwrap().take_element())); } /// Mark this object for teardown, and wake any thread awaiting an available element in `pop()`. @@ -218,7 +174,7 @@ impl<T> NotificationQueue<T> { if let Ok(ref mut this) = self.protected.try_lock() { let priority = Self::merge_priority_count(priority, this.count); this.count += 1; - this.heap.push(Prioritized{ element, priority }); + this.heap.push(Prioritized::new(priority, element)); } else { return Some(element); } @@ -235,7 +191,7 @@ impl<T> NotificationQueue<T> { let mut this = self.protected.lock().expect("the mutex is not poisoned"); let priority = Self::merge_priority_count(priority, this.count); this.count += 1; - this.heap.push(Prioritized{ element, priority }); + this.heap.push(Prioritized::new(priority, element)); } self.ready.notify_one(); } diff --git a/rustport/src/stlab/prioritized.rs b/rustport/src/stlab/prioritized.rs new file mode 100644 index 00000000..f00075b8 --- /dev/null +++ b/rustport/src/stlab/prioritized.rs @@ -0,0 +1,58 @@ +use std::cmp::{Ordering}; + +/// A `usize` constraining valid values to [0, 4) with runtime assertions. +#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] +pub struct Priority(pub usize); + +impl Priority { + pub fn new(value: usize) -> Self { + assert!((0..4).contains(&value), "Priorities must be in [0, 4)"); + Self(value) + } + + /// Returns a usize of the form 0bXX000000 where XX is a binary representation of this priority. + /// The value of this priority is guaranteed to fit in two bits because `Priority` values are constrained to [0b00, 0b11]. + pub fn to_highbit_mask(&self) -> usize { + &self.0 << (usize::BITS - 2) + } +} + +/// Pairs an instance of `T` with a `Priority`. +/// Equality and ordering of a Prioritized<T> only considers `priority`, disregarding `element`. +pub struct Prioritized <T> { + priority: Priority, + element: T +} + +impl <T> Prioritized<T> { + pub fn new(priority: Priority, element: T) -> Self { + Prioritized { priority, element } + } + + pub fn take_element(self) -> T { + self.element + } +} + +impl<T> PartialEq for Prioritized <T> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.priority == other.priority + } +} + +impl <T> Eq for Prioritized <T> {} + +impl<T> PartialOrd for Prioritized <T> { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + self.priority.partial_cmp(&other.priority) + } +} + +impl<T> Ord for Prioritized <T> { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.priority.cmp(&other.priority) + } +} \ No newline at end of file From 32ce01cb47191e21762602115c902c3790541086 Mon Sep 17 00:00:00 2001 From: nickpdemarco <nickpdemarco@gmail.com> Date: Fri, 3 Nov 2023 10:39:04 -0400 Subject: [PATCH 05/13] remove unsafe --- rustport/Cargo.lock | 281 ++++++++++-------------------- rustport/Cargo.toml | 2 + rustport/src/lib.rs | 45 +++-- rustport/src/stlab/mod.rs | 239 +++++++++++-------------- rustport/src/stlab/prioritized.rs | 3 +- 5 files changed, 229 insertions(+), 341 deletions(-) diff --git a/rustport/Cargo.lock b/rustport/Cargo.lock index 2a4be3dd..e97cef7c 100644 --- a/rustport/Cargo.lock +++ b/rustport/Cargo.lock @@ -8,7 +8,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi", "libc", "winapi", ] @@ -25,11 +25,17 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + [[package]] name = "cbindgen" -version = "0.24.3" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6358dedf60f4d9b8db43ad187391afe959746101346fe51bb978126bec61dfb" +checksum = "4b922faaf31122819ec80c4047cc684c6979a087366c069611e33649bf98e18d" dependencies = [ "clap", "heck", @@ -44,12 +50,6 @@ dependencies = [ "toml", ] -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" - [[package]] name = "cfg-if" version = "1.0.0" @@ -58,12 +58,12 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.2.24" +version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eef2b3ded6a26dfaec672a742c93c8cf6b689220324da509ec5caa20de55dc83" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", - "bitflags", + "bitflags 1.3.2", "clap_lex", "indexmap", "strsim", @@ -85,37 +85,24 @@ name = "default_executor" version = "0.1.0" dependencies = [ "cbindgen", + "once_cell", ] [[package]] name = "errno" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ - "cc", "libc", + "windows-sys", ] [[package]] name = "fastrand" -version = "1.9.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "hashbrown" @@ -138,12 +125,6 @@ dependencies = [ "libc", ] -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - [[package]] name = "indexmap" version = "1.9.3" @@ -154,131 +135,113 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" -dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "libc" -version = "0.2.142" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "linux-raw-sys" -version = "0.3.4" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "os_str_bytes" -version = "6.5.0" +version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "rustix" -version = "0.37.15" +version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ - "bitflags", + "bitflags 2.4.1", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.38", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -304,9 +267,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", @@ -315,22 +278,22 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.5.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] name = "termcolor" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" dependencies = [ "winapi-util", ] @@ -352,9 +315,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "winapi" @@ -374,9 +337,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -387,134 +350,68 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows-targets" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" -dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/rustport/Cargo.toml b/rustport/Cargo.toml index a58c1de4..4caf0fbd 100644 --- a/rustport/Cargo.toml +++ b/rustport/Cargo.toml @@ -9,6 +9,8 @@ crate-type = ["staticlib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +once_cell = "1.18.0" [build-dependencies] cbindgen = "0.24.0" + diff --git a/rustport/src/lib.rs b/rustport/src/lib.rs index 95f69f04..c80807d6 100644 --- a/rustport/src/lib.rs +++ b/rustport/src/lib.rs @@ -1,26 +1,43 @@ -use std::ffi::c_void; +use std::{ffi::c_void, sync::Mutex}; +use once_cell::sync::Lazy; +use stlab::PriorityTaskSystem; mod stlab; +static TASK_SYSTEM: Lazy<Mutex<PriorityTaskSystem>> = Lazy::new(|| Mutex::new(PriorityTaskSystem::new()) ); +struct ThreadsafeCFnWrapper { + context: *mut c_void, + fn_ptr: extern fn(*mut c_void) +} + +impl ThreadsafeCFnWrapper { + pub(crate) fn new(context: *mut c_void, fn_ptr: extern fn(*mut c_void)) -> Self { + Self { + context, + fn_ptr + } + } + + pub(crate) fn call(&self) { + (self.fn_ptr)(self.context) + } +} + +unsafe impl Send for ThreadsafeCFnWrapper {} + /// Enqueues a the execution of `f(context)` on the PriorityTaskSystem. #[no_mangle] -pub extern "C" fn execute(context: *mut c_void, f: extern fn(*mut c_void)) -> i32 { - stlab::PriorityTaskSystem::singleton().execute(move||{ - f(context) - }, stlab::Priority(0)); +pub extern "C" fn execute(context: *mut c_void, fn_ptr: extern fn(*mut c_void)) -> i32 { + execute_priority(context, fn_ptr, 0); 0 } /// Enqueues a the execution of `f(context)` on the PriorityTaskSystem at the given `priority`. #[no_mangle] -pub extern "C" fn execute_priority(context: *mut c_void, f: extern fn(*mut c_void), priority: usize) -> i32 { - stlab::PriorityTaskSystem::singleton().execute(move||{ - f(context) - }, stlab::Priority(priority)); +pub extern "C" fn execute_priority(context: *mut c_void, fn_ptr: extern fn(*mut c_void), p: usize) -> i32 { + let wrap = ThreadsafeCFnWrapper::new(context, fn_ptr); + TASK_SYSTEM.lock().unwrap().execute(move||{ + wrap.call() + }, stlab::Priority(p)); 0 } - -#[cfg(test)] -mod tests { - use super::*; -} diff --git a/rustport/src/stlab/mod.rs b/rustport/src/stlab/mod.rs index a98979c7..50995acc 100644 --- a/rustport/src/stlab/mod.rs +++ b/rustport/src/stlab/mod.rs @@ -1,14 +1,14 @@ use std::cmp::max; -use std::mem::MaybeUninit; use std::num::NonZeroUsize; -use std::sync::{Mutex, Once}; +use std::sync::{Mutex, Arc}; use std::sync::atomic::{AtomicUsize, Ordering as MemoryOrdering}; +use std::sync::OnceLock; mod prioritized; pub use prioritized::{Priority, Prioritized}; /// A type-erased, heap-allocated function object. -type Task = Box<dyn FnOnce()->()>; +type Task = Box<dyn FnOnce()->() + Send>; /// The fields of `Waiter` which must be protected by a `Mutex`. struct WaiterProtectedData { @@ -22,6 +22,12 @@ struct Waiter { ready: std::sync::Condvar, } +impl Default for Waiter { + fn default() -> Self { + Waiter::new() + } +} + impl Waiter { /// Constructs a new Waiter, with `waiting` and `done` set to `false`. @@ -124,7 +130,7 @@ impl<T> NotificationQueue<T> { /// Try to pop from the queue without blocking. /// Returns `None` if our mutex is already locked or if the queue is empty. - pub fn try_pop(&mut self) -> Option<T> { + pub fn try_pop(&self) -> Option<T> { if let Ok(ref mut this) = self.protected.try_lock() { if !this.heap.is_empty() { return Some(this.heap.pop().unwrap().take_element()); @@ -197,6 +203,7 @@ impl<T> NotificationQueue<T> { } } +unsafe impl <T> Sync for NotificationQueue<T> {} /// The fields of `PriorityTaskSystem` which must be protected by a `Mutex`. #[derive(Default)] struct PriorityTaskSystemProtectedData { @@ -204,176 +211,141 @@ struct PriorityTaskSystemProtectedData { waiters: Vec<Waiter>, } -impl PriorityTaskSystemProtectedData { - fn spawn_thread<F>(thread_name: &'static str, f: F) -> std::thread::JoinHandle<()> - where F: FnOnce() -> () + 'static + Send { - std::thread::Builder::new().name(thread_name.to_string()) - .spawn(f).expect("spawning a thread does not fail") - } - - /// Spawn `thread_count` threads, each of which pops tasks from an associated queue in `queues`, - /// or steals tasks from other queues if no task is available. - pub(self) fn spawn_baseline_threads(&mut self, thread_count: usize, available_parallelism: usize) { - self.threads.extend((0..thread_count).map(|i| { - Self::spawn_thread("cc.stlab.default_executor", move || { - loop { - let this = PriorityTaskSystem::singleton(); // why doesn't this have to be mut? - let mut task: Option<Task> = None; - for n in 0..available_parallelism { - match task { - Some(_) => { break } - _ => { - task = this.queues[(i + n) % available_parallelism].try_pop() - } - } - } - - if task.is_none() { - let done: bool; - (done, task) = this.queues[i].pop(); - if done { break } - } - - if task.is_some() { - task.unwrap()(); - } - } - }) - })) - } - - /// Spawn one thread which will repeatedly check each queue in `queues` for tasks, and execute them. - /// If no tasks are found, the thread is suspended. - pub(self) fn spawn_expansion_thread(&mut self, available_parallelism: usize) { - let protected = PriorityTaskSystem::singleton().protected.get_mut().expect("the mutex is not poisoned"); - let i = protected.threads.len(); - - if i == PriorityTaskSystem::singleton().available_parallelism { - eprintln!("Thread limit reached") - } - - self.threads.push(Self::spawn_thread("cc.stlab.default_executor.expansion", move || { - loop { - let this = PriorityTaskSystem::singleton(); // why doesn't this have to be mut? - let mut task: Option<Task> = None; - for n in 0..available_parallelism { - match task { - Some(_) => { break } - _ => { - task = this.queues[(i + n) % available_parallelism].try_pop() - } - } - } - - if task.is_some() { - task.unwrap()(); - continue; - } - - let protected = this.protected.get_mut().expect("the mutex is not poisoned"); - - // Note: The following means multiple threads may wait on a single `Waiter`. - if protected.waiters[i - available_parallelism].wait() { break; } - } - })); - } -} - /// A thread-scalable priority task system. pub struct PriorityTaskSystem { available_parallelism: usize, // _count in C++ implementation. thread_limit: usize, - queues: Vec<NotificationQueue<Task>>, + queues: Arc<Vec<NotificationQueue<Task>>>, index: AtomicUsize, - protected: Mutex<PriorityTaskSystemProtectedData> + protected: Arc<Mutex<PriorityTaskSystemProtectedData>> } impl PriorityTaskSystem { - - // TODO: Roll back the singleton pattern, and find a way to make instances immovable. - /// Return the singleton instance of `PriorityTaskSystem`, creating it on the first invocation. - pub fn singleton() -> &'static mut Self { - static mut INSTANCE: MaybeUninit<PriorityTaskSystem> = MaybeUninit::uninit(); - static ONCE: Once = Once::new(); - - ONCE.call_once(|| { - // This clunky expression is to convert from a NonZeroUsize to a usize for the subsequent arithmetic. I'd like a better way. - // SAFETY: we know 1 is not 0. - let nonzero_available_parallelism = std::thread::available_parallelism().unwrap_or(unsafe { NonZeroUsize::new_unchecked(1) }); - let available_parallelism = max(usize::from(nonzero_available_parallelism), 2) - 1; - let thread_limit = max(9, available_parallelism * 4 + 1); - - unsafe { - INSTANCE.as_mut_ptr().write(Self { - available_parallelism, - thread_limit, - queues: { - let mut v = Vec::with_capacity(available_parallelism); - for _ in 0..available_parallelism { v.push(NotificationQueue::<Task>::default()) } - v - }, - index: AtomicUsize::new(0), - protected: Mutex::new(PriorityTaskSystemProtectedData { - waiters: Vec::with_capacity(thread_limit - available_parallelism), - threads: Vec::with_capacity(available_parallelism), - }) - }); - } - // Defer spawning threads so they can never witness an uninitialized `INSTANCE`. - // Note `get_mut` statically enforces exclusive access; no locking will take place. - unsafe { - // `unwrap` should never panic; that would imply the mutex we just created, and never - // handed out, has been poisoned. - (*INSTANCE.as_mut_ptr()).protected.get_mut().unwrap().spawn_baseline_threads(available_parallelism, available_parallelism); - } + pub fn new() -> Self { + // SAFETY: We know 1 is not 0. + let nonzero_available_parallelism = std::thread::available_parallelism().unwrap_or(unsafe { NonZeroUsize::new_unchecked(1) }); + let available_parallelism = max(usize::from(nonzero_available_parallelism), 2) - 1; + let thread_limit = max(9, available_parallelism * 4 + 1); + + + let queues = Arc::new({ + let mut v = Vec::with_capacity(available_parallelism); + for _ in 0..available_parallelism { v.push(NotificationQueue::<Task>::default()) } + v }); - unsafe { &mut *INSTANCE.as_mut_ptr() } + Self { + available_parallelism, + thread_limit, + queues: queues.clone(), + index: AtomicUsize::new(0), + protected: Arc::new(Mutex::new(PriorityTaskSystemProtectedData { + waiters: (0..(thread_limit - available_parallelism)).map(|_| { Waiter::default() }).collect(), + threads: (0..available_parallelism).map(|i| { + let queues = queues.clone(); + std::thread::spawn(move ||{ + loop { + let mut task: Option<Task> = None; + for n in 0..available_parallelism { + match task { + Some(_) => { break } + _ => { + task = queues.get((i + n) % available_parallelism).unwrap().try_pop(); + } + } + } + + if task.is_none() { + let done: bool; + (done, task) = queues.get(i).unwrap().pop(); + if done { break } + } + + if task.is_some() { + task.unwrap()(); + } + } + }) + }).collect() + })) + } } /// Mark all subobjects for teardown, and join all threads spawned by this object. - pub fn join(&mut self) { - for queue in &self.queues { + pub fn join(&self) { + for queue in self.queues.iter() { queue.done(); } // Consume the contents under the mutex, leaving them empty. - let this = std::mem::take(self.protected.get_mut().expect("the mutex is not poisoned")); - for e in this.waiters { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + for e in &this.waiters { e.done(); } - for e in this.threads { + for e in std::mem::take(&mut this.threads) { let _ = e.join(); } - self.queues.clear(); + // self.queues.clear(); BIG REVISIT } /// Push `f` to the first queue in `queues` whose mutex is not under contention. /// If no such queue is found after a single pass, blockingly push `f` to one queue. // REVIEW: I'm not sure `execute` is a good name. I think we want `push`, or `push_with_priority`. - pub fn execute<F>(&mut self, f: F, p: Priority) where F: FnOnce() -> () + 'static { - let mut task: Option<Task> = Some(Box::new(f)); + pub fn execute<F>(&mut self, f: F, p: Priority) where F: FnOnce() -> () + Send + 'static { + self.execute_task(Box::new(f), p) + } + + pub fn execute_task(&mut self, task: Task, priority: Priority) { + let mut task: Option<Task> = Some(task); // Use SeqCst to match the default for C++'s std::atomic<unsigned>::operator++(int). let i = self.index.fetch_add(1, MemoryOrdering::SeqCst); for n in 0..i { - task = self.queues[(i + n) % self.available_parallelism].try_push(task.unwrap(), p); + task = self.queues.get((i + n) % self.available_parallelism).unwrap().try_push(task.unwrap(), priority); + // task = self.queues[(i + n) % self.available_parallelism].try_push(task.unwrap(), priority); if task.is_none() { return } } - self.queues[i % self.available_parallelism].push(task.unwrap(), p); + self.queues.get(i % self.available_parallelism).unwrap().push(task.unwrap(), priority); } /// Spawn an expansion thread which will steal tasks from `queues`. - pub fn add_thread(&mut self) { + pub fn add_thread(&self) { let mut this = self.protected.lock().expect("the mutex is not poisoned"); if this.threads.len() == self.thread_limit { eprintln!("Unable to add thread; thread_limit reached: {}", self.thread_limit); return; } - this.spawn_expansion_thread(self.available_parallelism); + let queues = self.queues.clone(); + let protected = self.protected.clone(); + let available_parallelism = self.available_parallelism.clone(); + let i = this.threads.len(); + + this.threads.push(std::thread::spawn(move ||{ + loop { + let mut task: Option<Task> = None; + for n in 0..available_parallelism { + match task { + Some(_) => { break } + _ => { + task = queues.get((i + n) % available_parallelism).unwrap().try_pop() + } + } + } + + if task.is_some() { + task.unwrap()(); + continue; + } + + let protected = protected.lock().expect("the mutex is not poisoned"); + + // Note: The following means multiple threads may wait on a single `Waiter`. + if protected.waiters[i - available_parallelism].wait() { break; } + } + })) } /// Get the number of waiters. @@ -384,13 +356,12 @@ impl PriorityTaskSystem { /// Wake every `Waiter` in `waiters`. Return true if any indicate they are `done()`, otherwise `false`. pub fn wake(&mut self) -> bool { - for queue in &self.queues { + for queue in self.queues.iter() { if queue.wake() { return true } } let size = self.waiters_size(); - // Note: get_mut ensures no locking occurs; uniqueness enforced by the type system. - let this = self.protected.get_mut().expect("the mutex is not poisoned"); + let this = self.protected.lock().expect("the mutex is not poisoned"); for n in 0..size { if this.waiters[n].wake() { return true; diff --git a/rustport/src/stlab/prioritized.rs b/rustport/src/stlab/prioritized.rs index f00075b8..fdf6cfd5 100644 --- a/rustport/src/stlab/prioritized.rs +++ b/rustport/src/stlab/prioritized.rs @@ -1,7 +1,8 @@ -use std::cmp::{Ordering}; +use std::cmp::Ordering; /// A `usize` constraining valid values to [0, 4) with runtime assertions. #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] +#[repr(C)] pub struct Priority(pub usize); impl Priority { From 33ab6fff245c6fab5b1f8a0b924b7c2374679655 Mon Sep 17 00:00:00 2001 From: nickpdemarco <nickpdemarco@gmail.com> Date: Mon, 20 Nov 2023 15:29:54 -0500 Subject: [PATCH 06/13] Version 2 of Rust Default Executor, with better abstractions and less unsafe --- rustport/cppshim/include/bindings.hpp | 14 +- rustport/src/lib.rs | 11 +- rustport/src/stlab/coarse_priority_queue.rs | 100 +++++ rustport/src/stlab/mod.rs | 389 ++++---------------- rustport/src/stlab/notification_queue.rs | 114 ++++++ rustport/src/stlab/prioritized.rs | 59 --- rustport/src/stlab/scoped_thread_pool.rs | 54 +++ rustport/src/stlab/waiter.rs | 75 ++++ 8 files changed, 438 insertions(+), 378 deletions(-) create mode 100644 rustport/src/stlab/coarse_priority_queue.rs create mode 100644 rustport/src/stlab/notification_queue.rs delete mode 100644 rustport/src/stlab/prioritized.rs create mode 100644 rustport/src/stlab/scoped_thread_pool.rs create mode 100644 rustport/src/stlab/waiter.rs diff --git a/rustport/cppshim/include/bindings.hpp b/rustport/cppshim/include/bindings.hpp index 03407ae1..224fa442 100644 --- a/rustport/cppshim/include/bindings.hpp +++ b/rustport/cppshim/include/bindings.hpp @@ -8,8 +8,16 @@ namespace stlab { inline namespace v1 { namespace detail { -// REVISIT - have to invert the priority encoding to match old API. -enum class executor_priority { high = 2, medium = 1, low = 0 }; +enum class executor_priority { high, medium, low }; + +inline auto bridge_priority(executor_priority p) -> Priority { + switch (p) { + case executor_priority::high: return Priority::High; + case executor_priority::medium: return Priority::Default; + case executor_priority::low: return Priority::Low; + default: return Priority::Default; + } +} /// @brief Asynchronously invoke f on the default_executor. /// @tparam F function object type @@ -39,7 +47,7 @@ auto enqueue_priority(F f, executor_priority p) { (*f)(); } catch (...) {} delete f; - }, static_cast<std::uint64_t>(p)); + }, bridge_priority(p)); } /// @brief A thin invokable wrapper around `enqueue_priority`. diff --git a/rustport/src/lib.rs b/rustport/src/lib.rs index c80807d6..f09e979c 100644 --- a/rustport/src/lib.rs +++ b/rustport/src/lib.rs @@ -1,10 +1,13 @@ use std::{ffi::c_void, sync::Mutex}; use once_cell::sync::Lazy; -use stlab::PriorityTaskSystem; +use stlab::{PriorityTaskSystem, Priority}; mod stlab; static TASK_SYSTEM: Lazy<Mutex<PriorityTaskSystem>> = Lazy::new(|| Mutex::new(PriorityTaskSystem::new()) ); + +// "Threadsafe" is not a guarantee, it is a requirement. These pointers are assumed to be able to +// be sent to another thread, and therefore must not rely on thread-local state. struct ThreadsafeCFnWrapper { context: *mut c_void, fn_ptr: extern fn(*mut c_void) @@ -28,16 +31,16 @@ unsafe impl Send for ThreadsafeCFnWrapper {} /// Enqueues a the execution of `f(context)` on the PriorityTaskSystem. #[no_mangle] pub extern "C" fn execute(context: *mut c_void, fn_ptr: extern fn(*mut c_void)) -> i32 { - execute_priority(context, fn_ptr, 0); + execute_priority(context, fn_ptr, Priority::Default); 0 } /// Enqueues a the execution of `f(context)` on the PriorityTaskSystem at the given `priority`. #[no_mangle] -pub extern "C" fn execute_priority(context: *mut c_void, fn_ptr: extern fn(*mut c_void), p: usize) -> i32 { +pub extern "C" fn execute_priority(context: *mut c_void, fn_ptr: extern fn(*mut c_void), p: Priority) -> i32 { let wrap = ThreadsafeCFnWrapper::new(context, fn_ptr); TASK_SYSTEM.lock().unwrap().execute(move||{ wrap.call() - }, stlab::Priority(p)); + }, p); 0 } diff --git a/rustport/src/stlab/coarse_priority_queue.rs b/rustport/src/stlab/coarse_priority_queue.rs new file mode 100644 index 00000000..c32676aa --- /dev/null +++ b/rustport/src/stlab/coarse_priority_queue.rs @@ -0,0 +1,100 @@ +use std::cmp::Ordering; + +#[derive(Eq, PartialEq, Copy, Clone)] +#[repr(C)] +pub enum Priority { + Low, + Default, + High +} + +impl Priority { + const fn highbit_mask(&self) -> usize { + match self { + Priority::Low => 0 << usize::BITS - 2, + Priority::Default => 1 << usize::BITS - 2, + Priority::High => 2 << usize::BITS - 2 + } + } + + fn merge_priority_count(&self, count: usize) -> usize { + self.highbit_mask() | count + } +} + +impl PartialOrd for Priority { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + self.highbit_mask().partial_cmp(&other.highbit_mask()) + } +} + +/// Pairs an instance of `T` with a `Priority`. +/// Equality and ordering of a Prioritized<T> only considers `priority`, disregarding `inner`. +struct Prioritized<T> { + inner: T, + priority: usize +} + +impl <T> Prioritized<T> { + pub fn new(inner: T, priority: Priority, count: usize) -> Self { + Prioritized { inner, priority: priority.merge_priority_count(count) } + } + + pub fn take_inner(self) -> T { + self.inner + } +} + +impl<T> PartialEq for Prioritized <T> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.priority == other.priority + } +} + +impl <T> Eq for Prioritized <T> {} + +impl<T> PartialOrd for Prioritized <T> { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + self.priority.partial_cmp(&other.priority) + } +} + +impl<T> Ord for Prioritized <T> { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.priority.cmp(&other.priority) + } +} + +// A priority queue with three priorities. Elements with equal priorities are popped in FIFO order. +pub struct CoarsePriorityQueue<T> { + inner: std::collections::BinaryHeap<Prioritized<T>>, + count: usize, +} + +impl <T> CoarsePriorityQueue<T> { + + pub fn new() -> Self { + Self { + inner: std::collections::BinaryHeap::new(), + count: 0 + } + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + pub fn pop(&mut self) -> Option<T> { + self.inner.pop().and_then(|prioritized| { + Some(prioritized.take_inner()) + }) + } + + pub fn push(&mut self, item: T, priority: Priority) { + self.inner.push(Prioritized::new(item, priority, self.count)); + self.count += 1; + } +} \ No newline at end of file diff --git a/rustport/src/stlab/mod.rs b/rustport/src/stlab/mod.rs index 50995acc..d3cc2152 100644 --- a/rustport/src/stlab/mod.rs +++ b/rustport/src/stlab/mod.rs @@ -1,329 +1,107 @@ use std::cmp::max; use std::num::NonZeroUsize; -use std::sync::{Mutex, Arc}; +use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering as MemoryOrdering}; -use std::sync::OnceLock; -mod prioritized; -pub use prioritized::{Priority, Prioritized}; +mod coarse_priority_queue; +pub use coarse_priority_queue::{Priority, CoarsePriorityQueue}; -/// A type-erased, heap-allocated function object. -type Task = Box<dyn FnOnce()->() + Send>; - -/// The fields of `Waiter` which must be protected by a `Mutex`. -struct WaiterProtectedData { - waiting: bool, - done: bool, -} - -/// A utility for suspending a thread using a condition variable. -struct Waiter { - protected: Mutex<WaiterProtectedData>, - ready: std::sync::Condvar, -} - -impl Default for Waiter { - fn default() -> Self { - Waiter::new() - } -} - -impl Waiter { - - /// Constructs a new Waiter, with `waiting` and `done` set to `false`. - pub fn new() -> Self { - Self { - protected: Mutex::new(WaiterProtectedData { - waiting: false, - done: false, - }), - ready: std::sync::Condvar::new() - } - } - - /// Sets `done` to `true`, and notifies one waiter of our condition variable. - pub fn done(&self) { - { - let mut this = self.protected.lock().expect("the mutex is not poisoned"); - this.done = true; - } - self.ready.notify_one(); - } - - /// Sets waiting to `false`. If waiting was `true`, wake one waiter and return `true`. Otherwise, return `false`. - /// If `try_lock` fails, return `false`. (REVIEW: why?) - /// (REVIEW: is it redundant to express that `waiting` and `done` are accesed under a mutex?) - pub fn wake(&self) -> bool { - if let Ok(ref mut this) = self.protected.try_lock() { - if !this.waiting { - return false; - } - this.waiting = false; - } else { - return false; - } - self.ready.notify_one(); - return true; - } - - /// Block this thread until `wake()` or `done()` is called. - /// Returns `true` if `done()` has been called, otherwise `false`. - pub fn wait(&self) -> bool { - let mut this = self.protected.lock().expect("the mutex is not poisoned"); - this.waiting = true; - while this.waiting && !this.done { - this = self.ready.wait(this).expect("the mutex is not poisoned"); - } - this.waiting = false; - return this.done; - } -} +mod scoped_thread_pool; +use scoped_thread_pool::ScopedThreadPool; -/// The fields of `NotificationQueue` which must be protected by a `Mutex`. -struct NotificationQueueProtectedData <T> { - heap: std::collections::BinaryHeap<Prioritized<T>>, - count: usize, - done: bool, - waiting: bool, -} +mod waiter; +pub use waiter::Waiter; -impl<T> std::default::Default for NotificationQueueProtectedData<T> { - fn default() -> Self { - Self { - heap: std::collections::BinaryHeap::new(), - count: 0, - done: false, - waiting: false - } - } -} - -/// A threadsafe priority queue. -struct NotificationQueue<T> { - // In the C++ implementation, we use a single lock for multiple data fields. - // In Rust, we require exactly one mutex per protected field. - // So, put protected fields into a separate struct, and lock on that. - // Note the transformation to use multiple locks is non-trivial, because - // this would require the ordering of acquired locks to be identical in all - // code paths to prevent deadlock. - protected: Mutex<NotificationQueueProtectedData<T>>, - ready: std::sync::Condvar, -} +pub mod notification_queue; +use notification_queue::NotificationQueue; -impl<T> std::default::Default for NotificationQueue<T> { - fn default() -> Self { - Self { - protected: Mutex::new(NotificationQueueProtectedData::<T>::default()), - ready: std::sync::Condvar::new(), - } - } +/// A type-erased, heap-allocated function object. +pub type Task = Box<dyn FnOnce()->() + Send>; +pub struct PriorityTaskSystem { + pool: ScopedThreadPool<Vec<NotificationQueue<Task>>>, + waiters: Arc<Vec<Waiter>>, + index: AtomicUsize, + available_parallelism: usize, + thread_limit: usize, } -impl<T> NotificationQueue<T> { - - /// Merge priority and count into a single usize, storing the former in the - /// two highest bits of the result. This requires priority be in [0, 4) i.e., - /// takes up two bits. - fn merge_priority_count(priority: Priority, count: usize) -> Priority { - Priority(priority.to_highbit_mask() | count) - } - - /// Try to pop from the queue without blocking. - /// Returns `None` if our mutex is already locked or if the queue is empty. - pub fn try_pop(&self) -> Option<T> { - if let Ok(ref mut this) = self.protected.try_lock() { - if !this.heap.is_empty() { - return Some(this.heap.pop().unwrap().take_element()); +impl Drop for PriorityTaskSystem { + fn drop(&mut self) { + self.pool.execute_immediately(|queues| { + for queue in queues.iter() { + queue.done() } - } - return None; - } - - /// If waiting in `pop()`, wakes and returns true. Otherwise returns false. - pub fn wake(&self) -> bool { - if let Ok(ref mut this) = self.protected.try_lock() { - if !this.waiting { - return false; - } - this.waiting = false; // triggers wake - } - return false; - } - - /// Pop from the queue, suspending the current thread until an element is available. - /// The returned `bool` indicates if this object is `done()`. - pub fn pop(&self) -> (bool, Option<T>) { - let mut this = self.protected.lock().expect("the mutex is not poisoned"); - this.waiting = true; - while !this.heap.is_empty() && !this.done && this.waiting { - this = self.ready.wait(this).expect("the mutex is not poisoned"); - } - this.waiting = false; - if this.heap.is_empty() { - return (this.done, None); - } - return (false, Some(this.heap.pop().unwrap().take_element())); - } - - /// Mark this object for teardown, and wake any thread awaiting an available element in `pop()`. - pub fn done(&self) { - { - let mut this = self.protected.lock().expect("the mutex is not poisoned"); - this.done = true - } - self.ready.notify_one(); - } - - /// Try to push `element` to the queue without blocking, returning `element` if our mutex is already locked. - /// If the push succeeds, wake a thread which may be awaiting an element in `pop()`. - pub fn try_push(&self, element: T, priority: Priority) -> Option<T> { - if let Ok(ref mut this) = self.protected.try_lock() { - let priority = Self::merge_priority_count(priority, this.count); - this.count += 1; - this.heap.push(Prioritized::new(priority, element)); - } else { - return Some(element); - } - - // We successfully locked the mutex, did our push, and released the lock. - self.ready.notify_one(); - return None; - } + }); - /// Push `element` to the queue, blocking if our mutex is already locked. - /// When the push succeeds, wake a thread which may be awaiting an element in `pop()`. - pub fn push(&self, element: T, priority: Priority) { - { - let mut this = self.protected.lock().expect("the mutex is not poisoned"); - let priority = Self::merge_priority_count(priority, this.count); - this.count += 1; - this.heap.push(Prioritized::new(priority, element)); + for waiter in self.waiters.iter() { + waiter.done() } - self.ready.notify_one(); } } -unsafe impl <T> Sync for NotificationQueue<T> {} -/// The fields of `PriorityTaskSystem` which must be protected by a `Mutex`. -#[derive(Default)] -struct PriorityTaskSystemProtectedData { - threads: Vec<std::thread::JoinHandle<()>>, - waiters: Vec<Waiter>, -} - -/// A thread-scalable priority task system. -pub struct PriorityTaskSystem { - available_parallelism: usize, // _count in C++ implementation. - thread_limit: usize, - queues: Arc<Vec<NotificationQueue<Task>>>, - index: AtomicUsize, - protected: Arc<Mutex<PriorityTaskSystemProtectedData>> -} - impl PriorityTaskSystem { - pub fn new() -> Self { // SAFETY: We know 1 is not 0. let nonzero_available_parallelism = std::thread::available_parallelism().unwrap_or(unsafe { NonZeroUsize::new_unchecked(1) }); let available_parallelism = max(usize::from(nonzero_available_parallelism), 2) - 1; let thread_limit = max(9, available_parallelism * 4 + 1); - - - let queues = Arc::new({ - let mut v = Vec::with_capacity(available_parallelism); - for _ in 0..available_parallelism { v.push(NotificationQueue::<Task>::default()) } - v - }); - Self { - available_parallelism, - thread_limit, - queues: queues.clone(), - index: AtomicUsize::new(0), - protected: Arc::new(Mutex::new(PriorityTaskSystemProtectedData { - waiters: (0..(thread_limit - available_parallelism)).map(|_| { Waiter::default() }).collect(), - threads: (0..available_parallelism).map(|i| { - let queues = queues.clone(); - std::thread::spawn(move ||{ - loop { - let mut task: Option<Task> = None; - for n in 0..available_parallelism { - match task { - Some(_) => { break } - _ => { - task = queues.get((i + n) % available_parallelism).unwrap().try_pop(); - } - } - } - - if task.is_none() { - let done: bool; - (done, task) = queues.get(i).unwrap().pop(); - if done { break } - } - - if task.is_some() { - task.unwrap()(); + pool: ScopedThreadPool::new(42, move |i, queues| { + loop { + let mut task: Option<Task> = None; + for n in 0..available_parallelism { + match task { + Some(_) => { break } + _ => { + task = queues.get((i + n) % available_parallelism).unwrap().try_pop(); } } - }) - }).collect() - })) - } - } + } - /// Mark all subobjects for teardown, and join all threads spawned by this object. - pub fn join(&self) { - for queue in self.queues.iter() { - queue.done(); - } + if task.is_none() { + let done: bool; + (done, task) = queues.get(i).unwrap().pop(); + if done { break } + } - // Consume the contents under the mutex, leaving them empty. - let mut this = self.protected.lock().expect("the mutex is not poisoned"); - for e in &this.waiters { - e.done(); - } - for e in std::mem::take(&mut this.threads) { - let _ = e.join(); + if task.is_some() { + task.unwrap()(); + } + } + }, (0..available_parallelism).map(|_| { NotificationQueue::default() }).collect()), + waiters: Arc::new((0..(thread_limit - available_parallelism)).map(|_| { Waiter::default() }).collect()), + index: AtomicUsize::new(0), + available_parallelism, + thread_limit } - - // self.queues.clear(); BIG REVISIT } /// Push `f` to the first queue in `queues` whose mutex is not under contention. /// If no such queue is found after a single pass, blockingly push `f` to one queue. // REVIEW: I'm not sure `execute` is a good name. I think we want `push`, or `push_with_priority`. - pub fn execute<F>(&mut self, f: F, p: Priority) where F: FnOnce() -> () + Send + 'static { + pub fn execute<F>(&self, f: F, p: Priority) where F: FnOnce() -> () + Send + 'static { self.execute_task(Box::new(f), p) } - pub fn execute_task(&mut self, task: Task, priority: Priority) { - let mut task: Option<Task> = Some(task); - // Use SeqCst to match the default for C++'s std::atomic<unsigned>::operator++(int). - let i = self.index.fetch_add(1, MemoryOrdering::SeqCst); - for n in 0..i { - task = self.queues.get((i + n) % self.available_parallelism).unwrap().try_push(task.unwrap(), priority); - // task = self.queues[(i + n) % self.available_parallelism].try_push(task.unwrap(), priority); - if task.is_none() { return } - } - - self.queues.get(i % self.available_parallelism).unwrap().push(task.unwrap(), priority); - } + pub fn execute_task(&self, task: Task, priority: Priority) { + self.pool.execute_immediately(|queues| { + let mut task: Option<Task> = Some(task); + let i = self.index.fetch_add(1, MemoryOrdering::SeqCst); + let n = self.available_parallelism; - /// Spawn an expansion thread which will steal tasks from `queues`. - pub fn add_thread(&self) { - let mut this = self.protected.lock().expect("the mutex is not poisoned"); - if this.threads.len() == self.thread_limit { - eprintln!("Unable to add thread; thread_limit reached: {}", self.thread_limit); - return; - } + for i in (i..i+n).map(|i| i % n) { + task = queues.get(i).unwrap().try_push(task.unwrap(), priority); + if task.is_none() { return } + } - let queues = self.queues.clone(); - let protected = self.protected.clone(); - let available_parallelism = self.available_parallelism.clone(); - let i = this.threads.len(); + queues.get(i % n).unwrap().push(task.unwrap(), priority); + }); + } - this.threads.push(std::thread::spawn(move ||{ + pub fn add_thread(&mut self) { + let waiters = self.waiters.clone(); + let available_parallelism = self.available_parallelism; + self.pool.add_thread(move |i, queues|{ loop { let mut task: Option<Task> = None; for n in 0..available_parallelism { @@ -340,34 +118,21 @@ impl PriorityTaskSystem { continue; } - let protected = protected.lock().expect("the mutex is not poisoned"); - // Note: The following means multiple threads may wait on a single `Waiter`. - if protected.waiters[i - available_parallelism].wait() { break; } - } - })) - } - - /// Get the number of waiters. - fn waiters_size(&self) -> usize { - let this = self.protected.lock().expect("the mutex is not poisoned"); - this.threads.len() - self.available_parallelism + if waiters[i - available_parallelism].wait() { break; } + } + }); } - /// Wake every `Waiter` in `waiters`. Return true if any indicate they are `done()`, otherwise `false`. - pub fn wake(&mut self) -> bool { - for queue in self.queues.iter() { - if queue.wake() { return true } - } - - let size = self.waiters_size(); - let this = self.protected.lock().expect("the mutex is not poisoned"); - for n in 0..size { - if this.waiters[n].wake() { - return true; - } - } + // Returns true if any thread was woken. + pub fn wake(&self) -> bool { + let any_queue_woken = self.pool.execute_immediately(|queues| { + return queues.iter().any(|queue| queue.wake()) + }); - return false; + if any_queue_woken { true } + else { self.waiters.iter().any(|waiter| waiter.wake()) } } } + +unsafe impl Send for PriorityTaskSystem {} \ No newline at end of file diff --git a/rustport/src/stlab/notification_queue.rs b/rustport/src/stlab/notification_queue.rs new file mode 100644 index 00000000..a344a412 --- /dev/null +++ b/rustport/src/stlab/notification_queue.rs @@ -0,0 +1,114 @@ +use crate::stlab::coarse_priority_queue::{Priority, CoarsePriorityQueue}; + +/// The fields of `NotificationQueue` which must be protected by a `Mutex`. +struct NotificationQueueProtectedData <T> { + queue: CoarsePriorityQueue<T>, + done: bool, + waiting: bool, +} + +impl<T> std::default::Default for NotificationQueueProtectedData<T> { + fn default() -> Self { + Self { + queue: CoarsePriorityQueue::new(), + done: false, + waiting: false + } + } +} + +/// A threadsafe priority queue. +pub struct NotificationQueue<T> { + // In the C++ implementation, we use a single lock for multiple data fields. + // In Rust, we require exactly one mutex per protected field. + // So, put protected fields into a separate struct, and lock on that. + // Note the transformation to use multiple locks is non-trivial, because + // this would require the ordering of acquired locks to be identical in all + // code paths to prevent deadlock. + protected: std::sync::Mutex<NotificationQueueProtectedData<T>>, + ready: std::sync::Condvar, +} + +impl<T> std::default::Default for NotificationQueue<T> { + fn default() -> Self { + Self { + protected: std::sync::Mutex::new(NotificationQueueProtectedData::<T>::default()), + ready: std::sync::Condvar::new(), + } + } +} + +impl<T> NotificationQueue<T> { + + /// Try to pop from the queue without blocking. + /// Returns `None` if our mutex is already locked or if the queue is empty. + pub fn try_pop(&self) -> Option<T> { + if let Ok(ref mut this) = self.protected.try_lock() { + return this.queue.pop(); + } + return None; + } + + /// If waiting in `pop()`, wakes and returns true. Otherwise returns false. + pub fn wake(&self) -> bool { + if let Ok(ref mut this) = self.protected.try_lock() { + if !this.waiting { + return false; + } + this.waiting = false; // triggers wake + } + self.ready.notify_one(); + return true; + } + + /// Pop from the queue, suspending the current thread until an element is available. + /// The returned `bool` indicates if this object is `done()`. + pub fn pop(&self) -> (bool, Option<T>) { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.waiting = true; + while !this.queue.is_empty() && !this.done && this.waiting { + this = self.ready.wait(this).expect("the mutex is not poisoned"); + } + this.waiting = false; + if this.queue.is_empty() { + return (this.done, None); + } + return (false, this.queue.pop()); + } + + /// Mark this object for teardown, and wake any thread awaiting an available element in `pop()`. + pub fn done(&self) { + { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.done = true + } + self.ready.notify_one(); + } + + /// Try to push `element` to the queue without blocking, returning `element` if our mutex is already locked. + /// If the push succeeds, wake a thread which may be awaiting an element in `pop()`. + pub fn try_push(&self, element: T, priority: Priority) -> Option<T> { + if let Ok(ref mut this) = self.protected.try_lock() { + this.queue.push(element, priority); + } else { + return Some(element); + } + + // We successfully locked the mutex, did our push, and released the lock. + self.ready.notify_one(); + return None; + } + + /// Push `element` to the queue, blocking if our mutex is already locked. + /// When the push succeeds, wake a thread which may be awaiting an element in `pop()`. + pub fn push(&self, element: T, priority: Priority) { + { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.queue.push(element, priority); + } + self.ready.notify_one(); + } +} + +unsafe impl <T> Send for NotificationQueue<T> {} +unsafe impl <T> Sync for NotificationQueue<T> {} \ No newline at end of file diff --git a/rustport/src/stlab/prioritized.rs b/rustport/src/stlab/prioritized.rs deleted file mode 100644 index fdf6cfd5..00000000 --- a/rustport/src/stlab/prioritized.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::cmp::Ordering; - -/// A `usize` constraining valid values to [0, 4) with runtime assertions. -#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] -#[repr(C)] -pub struct Priority(pub usize); - -impl Priority { - pub fn new(value: usize) -> Self { - assert!((0..4).contains(&value), "Priorities must be in [0, 4)"); - Self(value) - } - - /// Returns a usize of the form 0bXX000000 where XX is a binary representation of this priority. - /// The value of this priority is guaranteed to fit in two bits because `Priority` values are constrained to [0b00, 0b11]. - pub fn to_highbit_mask(&self) -> usize { - &self.0 << (usize::BITS - 2) - } -} - -/// Pairs an instance of `T` with a `Priority`. -/// Equality and ordering of a Prioritized<T> only considers `priority`, disregarding `element`. -pub struct Prioritized <T> { - priority: Priority, - element: T -} - -impl <T> Prioritized<T> { - pub fn new(priority: Priority, element: T) -> Self { - Prioritized { priority, element } - } - - pub fn take_element(self) -> T { - self.element - } -} - -impl<T> PartialEq for Prioritized <T> { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.priority == other.priority - } -} - -impl <T> Eq for Prioritized <T> {} - -impl<T> PartialOrd for Prioritized <T> { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - self.priority.partial_cmp(&other.priority) - } -} - -impl<T> Ord for Prioritized <T> { - #[inline] - fn cmp(&self, other: &Self) -> Ordering { - self.priority.cmp(&other.priority) - } -} \ No newline at end of file diff --git a/rustport/src/stlab/scoped_thread_pool.rs b/rustport/src/stlab/scoped_thread_pool.rs new file mode 100644 index 00000000..ea32bced --- /dev/null +++ b/rustport/src/stlab/scoped_thread_pool.rs @@ -0,0 +1,54 @@ +use std::thread::JoinHandle; + +pub struct ScopedThreadPool<T: Default + Send + Sync + 'static> { + threads: Vec<JoinHandle<()>>, + // SAFETY: We store this pointer as a `*mut` so it can be dropped via Box::from_raw, but + // we only ever hand out const-refs to the underlying data. + data: *mut T +} + +impl <T: Default + Send + Sync + 'static> Drop for ScopedThreadPool <T> { + fn drop(&mut self) { + for thread in std::mem::take(&mut self.threads) { + let _ = thread.join(); // TODO handle error? what's the rule for drop? + } + + // SAFETY: We only call from_raw once, in this `drop`. We do not permit copies of this + // object, which would present the risk of a double-free on the copied pointer. + unsafe { drop(Box::from_raw(self.data)) }; + } +} + +impl <T: Default + Send + Sync + 'static> ScopedThreadPool <T> { + pub fn new<F: Fn(usize, &T) + Copy + Send + 'static>(num_threads: usize, f: F, data: T) -> Self { + let data = Box::into_raw(Box::new(data)); + Self { + threads: (0..num_threads).map(|i| { + let f = f.clone(); + // SAFETY: We only hand out immutable references to this data. + let data: &T = unsafe { &*data }; + std::thread::spawn(move || { + f(i, data); + }) + }).collect(), + data + } + } + + pub fn execute_immediately<Return, F: FnOnce(&T) -> Return>(&self, f: F) -> Return { + // SAFETY: We only hand out immutable references to this data. + let data: &T = unsafe { &*self.data }; + f(data) + } + + pub fn add_thread<F: Fn(usize, &T) + Send + 'static>(&mut self, f: F) { + let i = self.threads.len(); + self.threads.push({ + // SAFETY: We only hand out immutable references to this data. + let data: &T = unsafe { &*self.data }; + std::thread::spawn(move || { + f(i, data); + }) + }); + } +} \ No newline at end of file diff --git a/rustport/src/stlab/waiter.rs b/rustport/src/stlab/waiter.rs new file mode 100644 index 00000000..59947507 --- /dev/null +++ b/rustport/src/stlab/waiter.rs @@ -0,0 +1,75 @@ +use std::sync::Mutex; + +// REVISIT: It may be wise to reimplement this in terms of std::thread::park. + +/// The fields of `Waiter` which must be protected by a `Mutex`. +struct WaiterProtectedData { + waiting: bool, + done: bool, +} + +/// A utility for suspending a thread using a condition variable. +pub struct Waiter { + protected: Mutex<WaiterProtectedData>, + ready: std::sync::Condvar, +} + +impl Default for Waiter { + fn default() -> Self { + Waiter::new() + } +} + +impl Waiter { + + /// Constructs a new Waiter, with `waiting` and `done` set to `false`. + pub fn new() -> Self { + Self { + protected: Mutex::new(WaiterProtectedData { + waiting: false, + done: false, + }), + ready: std::sync::Condvar::new() + } + } + + /// Sets `done` to `true`, and notifies one waiter of our condition variable. + pub fn done(&self) { + { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.done = true; + } + self.ready.notify_one(); + } + + /// Sets waiting to `false`. If waiting was `true`, wake one waiter and return `true`. Otherwise, return `false`. + /// If `try_lock` fails, return `false`. (REVIEW: why?) + /// (REVIEW: is it redundant to express that `waiting` and `done` are accesed under a mutex?) + pub fn wake(&self) -> bool { + if let Ok(ref mut this) = self.protected.try_lock() { + if !this.waiting { + return false; + } + this.waiting = false; + } else { + return false; + } + self.ready.notify_one(); + return true; + } + + /// Block this thread until `wake()` or `done()` is called. + /// Returns `true` if `done()` has been called, otherwise `false`. + pub fn wait(&self) -> bool { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.waiting = true; + while this.waiting && !this.done { + this = self.ready.wait(this).expect("the mutex is not poisoned"); + } + this.waiting = false; + return this.done; + } +} + +unsafe impl Send for Waiter {} +unsafe impl Sync for Waiter {} \ No newline at end of file From ca418b8cfbee8d2830e49dd2126d315d36f2c636 Mon Sep 17 00:00:00 2001 From: nickpdemarco <nickpdemarco@gmail.com> Date: Tue, 28 Nov 2023 17:41:09 -0500 Subject: [PATCH 07/13] Contracts --- rustport/src/stlab/coarse_priority_queue.rs | 81 +++++++++------ rustport/src/stlab/drop_join_thread_pool.rs | 101 +++++++++++++++++++ rustport/src/stlab/mod.rs | 106 +++++++++++--------- rustport/src/stlab/notification_queue.rs | 42 ++++---- rustport/src/stlab/scoped_thread_pool.rs | 54 ---------- rustport/src/stlab/waiter.rs | 14 +-- 6 files changed, 236 insertions(+), 162 deletions(-) create mode 100644 rustport/src/stlab/drop_join_thread_pool.rs delete mode 100644 rustport/src/stlab/scoped_thread_pool.rs diff --git a/rustport/src/stlab/coarse_priority_queue.rs b/rustport/src/stlab/coarse_priority_queue.rs index c32676aa..a14166ab 100644 --- a/rustport/src/stlab/coarse_priority_queue.rs +++ b/rustport/src/stlab/coarse_priority_queue.rs @@ -1,5 +1,40 @@ use std::cmp::Ordering; +/// A priority queue with three priorities. Elements with equal priorities are popped in FIFO order. +pub struct CoarsePriorityQueue<T> { + inner: std::collections::BinaryHeap<Prioritized<T>>, + count: usize, +} + +impl <T> CoarsePriorityQueue<T> { + /// Creates a new, empty `CoarsePriorityQueue`. + pub fn new() -> Self { + Self { + inner: std::collections::BinaryHeap::new(), + count: 0 + } + } + + /// Checks if the queue is empty. + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// Removes the greatest item from the queue at returns it, or None if it is empty. + pub fn pop(&mut self) -> Option<T> { + self.inner.pop().and_then(|prioritized| { + Some(prioritized.take_inner()) + }) + } + + /// Pushes an item onto the queue at the given `priority`. + pub fn push(&mut self, item: T, priority: Priority) { + self.inner.push(Prioritized::new(item, priority, self.count)); + self.count += 1; + } +} + +/// The three priorities used by `CoarsePriorityQueue`. #[derive(Eq, PartialEq, Copy, Clone)] #[repr(C)] pub enum Priority { @@ -8,6 +43,16 @@ pub enum Priority { High } +/// Use of `Priority` and `Prioritized` requires the client maintain a nondecreasing count which +/// is supplied to the constructor of `Prioritized<T>`. That count is bitwise-OR'd with the +/// result of `highbit_mask` for a given `Priority`. The resulting `usize` is used for ordering and +/// equality of a `Prioritized<T>`. The purpose of this is to implement FIFO ordering (with coarse +/// priorities) with a single BinaryHeap. +/// +/// Note: This implies the value of the count must not exceed +/// 0b001111111111111111111111111111111111111111111111111111111111111, or 2,305,843,009,213,693,951, +/// but this is considered unlikely enough to not be exposed in the public documentation for +/// CoarsePriorityQueue. impl Priority { const fn highbit_mask(&self) -> usize { match self { @@ -17,6 +62,7 @@ impl Priority { } } + /// Precondition: count < 2,305,843,009,213,693,951 fn merge_priority_count(&self, count: usize) -> usize { self.highbit_mask() | count } @@ -29,13 +75,15 @@ impl PartialOrd for Priority { } /// Pairs an instance of `T` with a `Priority`. -/// Equality and ordering of a Prioritized<T> only considers `priority`, disregarding `inner`. +/// Equality and ordering of a Prioritized<T> only considers `priority`, and disregards `inner`. struct Prioritized<T> { inner: T, priority: usize } impl <T> Prioritized<T> { + + /// Precondition: count must be less than 2,305,843,009,213,693,951. pub fn new(inner: T, priority: Priority, count: usize) -> Self { Prioritized { inner, priority: priority.merge_priority_count(count) } } @@ -66,35 +114,4 @@ impl<T> Ord for Prioritized <T> { fn cmp(&self, other: &Self) -> Ordering { self.priority.cmp(&other.priority) } -} - -// A priority queue with three priorities. Elements with equal priorities are popped in FIFO order. -pub struct CoarsePriorityQueue<T> { - inner: std::collections::BinaryHeap<Prioritized<T>>, - count: usize, -} - -impl <T> CoarsePriorityQueue<T> { - - pub fn new() -> Self { - Self { - inner: std::collections::BinaryHeap::new(), - count: 0 - } - } - - pub fn is_empty(&self) -> bool { - self.inner.is_empty() - } - - pub fn pop(&mut self) -> Option<T> { - self.inner.pop().and_then(|prioritized| { - Some(prioritized.take_inner()) - }) - } - - pub fn push(&mut self, item: T, priority: Priority) { - self.inner.push(Prioritized::new(item, priority, self.count)); - self.count += 1; - } } \ No newline at end of file diff --git a/rustport/src/stlab/drop_join_thread_pool.rs b/rustport/src/stlab/drop_join_thread_pool.rs new file mode 100644 index 00000000..5e271b61 --- /dev/null +++ b/rustport/src/stlab/drop_join_thread_pool.rs @@ -0,0 +1,101 @@ +use std::{thread::JoinHandle, io::Write}; + +/// A thread pool which joins all spawned threads when dropped. +/// +/// This pool also holds a pointer to a heap-allocated object which is shared by all spawned +/// threads. As such, it must be Send + Sync, and 'static (having no non-static references). When +/// tasks in this pool are spawned, they are passed an immutable reference to said object (there is +/// no way to acquire a mutable reference). +/// +/// Note: it remains to be seen if it is possible (or useful) to loosen the 'static requirement here +/// to instead allow any lifetimes which do not outlive this pool. Early attempts at this polluted +/// the API with explicit lifetimes. See +/// https://users.rust-lang.org/t/access-to-implicit-lifetime-of-containing-object-aka-self-lifetime/18917 +/// for a relevant disucssion. +pub struct DropJoinThreadPool<T: Default + Send + Sync + 'static> { + threads: Vec<JoinHandle<()>>, + // SAFETY: We store this pointer as a `*mut` so it can be dropped via Box::from_raw, but + // we only ever hand out immutable references to the pointee. + data: *mut T +} + +impl <T: Default + Send + Sync + 'static> Drop for DropJoinThreadPool <T> { + + /// Join all spawned threads, and drop `data` manually. + fn drop(&mut self) { + for thread in std::mem::take(&mut self.threads) { + let _ = thread.join(); // TODO handle error? what's the rule for drop? + } + + // SAFETY: We only call from_raw once, in this `drop`. We do not permit copies of this + // object, which would present the risk of a double-free on the copied pointer. + unsafe { drop(Box::from_raw(self.data)) }; + } +} + +impl <T: Default + Send + Sync + 'static> DropJoinThreadPool <T> { + + /// Creates a new `DropJoinThreadPool` with a maximum thread capacity of `thread_limit`, moving + /// `data` onto the heap. + pub fn new(thread_limit: usize, data: T) -> Self { + Self { + threads: Vec::<JoinHandle<()>>::with_capacity(thread_limit), + data: Box::into_raw(Box::new(data)) + } + } + + /// Performs an operation with an immutable reference to this pool's `data` on the current + /// thread. + pub fn execute_immediately<Return, F: FnOnce(&T) -> Return>(&self, f: F) -> Return { + // SAFETY: We only hand out immutable references to this data. + let data: &T = unsafe { &*self.data }; + f(data) + } + + /// Spawns at most `n` threads with the task `f` until this pool's thread capacity is reached. + /// + /// If spawning all `n` threads would exceed this pool's thread capacity, a message is logged to + /// stderr. + pub fn spawn_n<F: Fn(usize, &T) + Copy + Send + 'static>(&mut self, f: F, mut n: usize) { + if self.threads.len() + n > self.threads.capacity() { + Self::log_err(b"stlab: Unable to spawn all threads; capacity reached."); + n = self.threads.capacity() - self.threads.len(); + } + + self.threads.extend((0..n).map(|i| { + let f = f.clone(); + // SAFETY: We only hand out immutable references to this data. + let data: &T = unsafe { &*self.data }; + std::thread::spawn(move||{ + f(i, data); + }) + })); + } + + /// Spawns a single thread with the task `f`, unless doing so would exceed this pool's thread + /// capacity. + /// + /// If the thread is not spawned due to inadequate space, a message is logged to stderr. + pub fn spawn<F: Fn(usize, &T) + Send + 'static>(&mut self, f: F) { + let i = self.threads.len(); + + if i == self.threads.capacity() { + Self::log_err(b"stlab: Unable to spawn thread; capacity reached."); + return; + } + + self.threads.push({ + // SAFETY: We only hand out immutable references to this data. + let data: &T = unsafe { &*self.data }; + std::thread::spawn(move || { + f(i, data); + }) + }); + } + + /// Write `buf` to stderr, discarding the ioresult. + fn log_err(buf: &[u8]) { + let mut stderr = std::io::stderr().lock(); + let _ = stderr.write_all(buf); + } +} \ No newline at end of file diff --git a/rustport/src/stlab/mod.rs b/rustport/src/stlab/mod.rs index d3cc2152..14caee35 100644 --- a/rustport/src/stlab/mod.rs +++ b/rustport/src/stlab/mod.rs @@ -4,10 +4,10 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering as MemoryOrdering}; mod coarse_priority_queue; -pub use coarse_priority_queue::{Priority, CoarsePriorityQueue}; +pub use coarse_priority_queue::Priority; -mod scoped_thread_pool; -use scoped_thread_pool::ScopedThreadPool; +mod drop_join_thread_pool; +use drop_join_thread_pool::DropJoinThreadPool; mod waiter; pub use waiter::Waiter; @@ -17,12 +17,25 @@ use notification_queue::NotificationQueue; /// A type-erased, heap-allocated function object. pub type Task = Box<dyn FnOnce()->() + Send>; + +/// A portable work-stealing task scheduler with three priorities. +/// +/// By default, this scheduler spins up a number of threads corresponding to the amount of +/// parallelism available on the target platform, namely, std::thread::available_parallelism() - 1. +/// Each thread is assigned a threadsafe priority queue. To reduce contention on push and pop +/// operations, a thread will first attempt to acquire the lock for its own queue without blocking. +/// If that fails, it will attempt the same non-blocking push/pop for each other priority queue in +/// the scheduler. Finally, if each of those attempts also fail, the thread will attempt a blocking +/// push/pop on its own priority queue. +/// +/// The `add_thread` API is intended to mitigate the possibility of deadlock by spinning up a new +/// worker thread that non-blockingly polls all of the system's priority queues, and then sleeps +/// until `wake()` is called. pub struct PriorityTaskSystem { - pool: ScopedThreadPool<Vec<NotificationQueue<Task>>>, + pool: DropJoinThreadPool<Vec<NotificationQueue<Task>>>, waiters: Arc<Vec<Waiter>>, index: AtomicUsize, available_parallelism: usize, - thread_limit: usize, } impl Drop for PriorityTaskSystem { @@ -40,39 +53,36 @@ impl Drop for PriorityTaskSystem { } impl PriorityTaskSystem { + /// Creates a new PriorityTaskSystem. pub fn new() -> Self { // SAFETY: We know 1 is not 0. let nonzero_available_parallelism = std::thread::available_parallelism().unwrap_or(unsafe { NonZeroUsize::new_unchecked(1) }); let available_parallelism = max(usize::from(nonzero_available_parallelism), 2) - 1; let thread_limit = max(9, available_parallelism * 4 + 1); - Self { - pool: ScopedThreadPool::new(42, move |i, queues| { - loop { - let mut task: Option<Task> = None; - for n in 0..available_parallelism { - match task { - Some(_) => { break } - _ => { - task = queues.get((i + n) % available_parallelism).unwrap().try_pop(); - } - } - } - - if task.is_none() { - let done: bool; - (done, task) = queues.get(i).unwrap().pop(); - if done { break } - } - - if task.is_some() { - task.unwrap()(); - } + let queues = (0..available_parallelism).map(|_| { NotificationQueue::default() }).collect(); + + let mut pool = DropJoinThreadPool::new(thread_limit, queues); + pool.spawn_n(move |i, queues| { + loop { + let mut task = Self::try_pop(queues, i, available_parallelism); + + if task.is_none() { + let done: bool; + (done, task) = queues.get(i).unwrap().pop(); + if done { break } } - }, (0..available_parallelism).map(|_| { NotificationQueue::default() }).collect()), + + if task.is_some() { + task.unwrap()(); + } + } + }, available_parallelism); + + Self { + pool, waiters: Arc::new((0..(thread_limit - available_parallelism)).map(|_| { Waiter::default() }).collect()), index: AtomicUsize::new(0), available_parallelism, - thread_limit } } @@ -89,37 +99,30 @@ impl PriorityTaskSystem { let i = self.index.fetch_add(1, MemoryOrdering::SeqCst); let n = self.available_parallelism; + // Attempt to push to a queue without blocking, starting with ours. for i in (i..i+n).map(|i| i % n) { task = queues.get(i).unwrap().try_push(task.unwrap(), priority); - if task.is_none() { return } + if task.is_none() { return } // An empty return means push was successful. } + // Otherwise, attempt to push to our queue, with blocking. queues.get(i % n).unwrap().push(task.unwrap(), priority); }); } + /// Add a work-stealing thread to the scheduler to mitigate deadlock. pub fn add_thread(&mut self) { let waiters = self.waiters.clone(); - let available_parallelism = self.available_parallelism; - self.pool.add_thread(move |i, queues|{ + let n = self.available_parallelism; + self.pool.spawn(move |i, queues|{ loop { - let mut task: Option<Task> = None; - for n in 0..available_parallelism { - match task { - Some(_) => { break } - _ => { - task = queues.get((i + n) % available_parallelism).unwrap().try_pop() - } - } - } - - if task.is_some() { - task.unwrap()(); + if let Some(task) = Self::try_pop(queues, i, n) { + task(); continue; } // Note: The following means multiple threads may wait on a single `Waiter`. - if waiters[i - available_parallelism].wait() { break; } + if waiters[i - n].wait() { break; } } }); } @@ -133,6 +136,19 @@ impl PriorityTaskSystem { if any_queue_woken { true } else { self.waiters.iter().any(|waiter| waiter.wake()) } } + + /// Attempt to non-blockingly pop a task from each queue in the system, starting at index + /// `starting_at`. + fn try_pop(queues: &Vec<NotificationQueue<Task>>, starting_at: usize, modulo: usize) -> Option<Task> { + for i in (starting_at..starting_at+modulo).map(|i| i % modulo) { + match queues.get(i).unwrap().try_pop() { + Some(t) => Some(t), + None => continue + }; + } + None + } + } unsafe impl Send for PriorityTaskSystem {} \ No newline at end of file diff --git a/rustport/src/stlab/notification_queue.rs b/rustport/src/stlab/notification_queue.rs index a344a412..c9807ee5 100644 --- a/rustport/src/stlab/notification_queue.rs +++ b/rustport/src/stlab/notification_queue.rs @@ -1,30 +1,7 @@ use crate::stlab::coarse_priority_queue::{Priority, CoarsePriorityQueue}; -/// The fields of `NotificationQueue` which must be protected by a `Mutex`. -struct NotificationQueueProtectedData <T> { - queue: CoarsePriorityQueue<T>, - done: bool, - waiting: bool, -} - -impl<T> std::default::Default for NotificationQueueProtectedData<T> { - fn default() -> Self { - Self { - queue: CoarsePriorityQueue::new(), - done: false, - waiting: false - } - } -} - /// A threadsafe priority queue. pub struct NotificationQueue<T> { - // In the C++ implementation, we use a single lock for multiple data fields. - // In Rust, we require exactly one mutex per protected field. - // So, put protected fields into a separate struct, and lock on that. - // Note the transformation to use multiple locks is non-trivial, because - // this would require the ordering of acquired locks to be identical in all - // code paths to prevent deadlock. protected: std::sync::Mutex<NotificationQueueProtectedData<T>>, ready: std::sync::Condvar, } @@ -111,4 +88,21 @@ impl<T> NotificationQueue<T> { } unsafe impl <T> Send for NotificationQueue<T> {} -unsafe impl <T> Sync for NotificationQueue<T> {} \ No newline at end of file +unsafe impl <T> Sync for NotificationQueue<T> {} + +/// The fields of `NotificationQueue` which must be protected by a `Mutex`. +struct NotificationQueueProtectedData <T> { + queue: CoarsePriorityQueue<T>, + done: bool, + waiting: bool, +} + +impl<T> std::default::Default for NotificationQueueProtectedData<T> { + fn default() -> Self { + Self { + queue: CoarsePriorityQueue::new(), + done: false, + waiting: false + } + } +} diff --git a/rustport/src/stlab/scoped_thread_pool.rs b/rustport/src/stlab/scoped_thread_pool.rs deleted file mode 100644 index ea32bced..00000000 --- a/rustport/src/stlab/scoped_thread_pool.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::thread::JoinHandle; - -pub struct ScopedThreadPool<T: Default + Send + Sync + 'static> { - threads: Vec<JoinHandle<()>>, - // SAFETY: We store this pointer as a `*mut` so it can be dropped via Box::from_raw, but - // we only ever hand out const-refs to the underlying data. - data: *mut T -} - -impl <T: Default + Send + Sync + 'static> Drop for ScopedThreadPool <T> { - fn drop(&mut self) { - for thread in std::mem::take(&mut self.threads) { - let _ = thread.join(); // TODO handle error? what's the rule for drop? - } - - // SAFETY: We only call from_raw once, in this `drop`. We do not permit copies of this - // object, which would present the risk of a double-free on the copied pointer. - unsafe { drop(Box::from_raw(self.data)) }; - } -} - -impl <T: Default + Send + Sync + 'static> ScopedThreadPool <T> { - pub fn new<F: Fn(usize, &T) + Copy + Send + 'static>(num_threads: usize, f: F, data: T) -> Self { - let data = Box::into_raw(Box::new(data)); - Self { - threads: (0..num_threads).map(|i| { - let f = f.clone(); - // SAFETY: We only hand out immutable references to this data. - let data: &T = unsafe { &*data }; - std::thread::spawn(move || { - f(i, data); - }) - }).collect(), - data - } - } - - pub fn execute_immediately<Return, F: FnOnce(&T) -> Return>(&self, f: F) -> Return { - // SAFETY: We only hand out immutable references to this data. - let data: &T = unsafe { &*self.data }; - f(data) - } - - pub fn add_thread<F: Fn(usize, &T) + Send + 'static>(&mut self, f: F) { - let i = self.threads.len(); - self.threads.push({ - // SAFETY: We only hand out immutable references to this data. - let data: &T = unsafe { &*self.data }; - std::thread::spawn(move || { - f(i, data); - }) - }); - } -} \ No newline at end of file diff --git a/rustport/src/stlab/waiter.rs b/rustport/src/stlab/waiter.rs index 59947507..30712389 100644 --- a/rustport/src/stlab/waiter.rs +++ b/rustport/src/stlab/waiter.rs @@ -2,12 +2,6 @@ use std::sync::Mutex; // REVISIT: It may be wise to reimplement this in terms of std::thread::park. -/// The fields of `Waiter` which must be protected by a `Mutex`. -struct WaiterProtectedData { - waiting: bool, - done: bool, -} - /// A utility for suspending a thread using a condition variable. pub struct Waiter { protected: Mutex<WaiterProtectedData>, @@ -72,4 +66,10 @@ impl Waiter { } unsafe impl Send for Waiter {} -unsafe impl Sync for Waiter {} \ No newline at end of file +unsafe impl Sync for Waiter {} + +/// The fields of `Waiter` which must be protected by a `Mutex`. +struct WaiterProtectedData { + waiting: bool, + done: bool, +} \ No newline at end of file From 7aa74bc7e7849e61291f172a4656609c561ea574 Mon Sep 17 00:00:00 2001 From: nickpdemarco <nickpdemarco@gmail.com> Date: Tue, 28 Nov 2023 18:28:39 -0500 Subject: [PATCH 08/13] cleanup --- rustport/cppshim/include/bindings.hpp | 50 ++++---------------------- rustport/src/stlab/mod.rs | 4 +-- stlab/concurrency/default_executor.hpp | 40 ++++++++++++++++----- 3 files changed, 40 insertions(+), 54 deletions(-) diff --git a/rustport/cppshim/include/bindings.hpp b/rustport/cppshim/include/bindings.hpp index 224fa442..f6b88b30 100644 --- a/rustport/cppshim/include/bindings.hpp +++ b/rustport/cppshim/include/bindings.hpp @@ -4,22 +4,9 @@ #include <type_traits> #include <utility> -namespace stlab { -inline namespace v1 { -namespace detail { +namespace rust { -enum class executor_priority { high, medium, low }; - -inline auto bridge_priority(executor_priority p) -> Priority { - switch (p) { - case executor_priority::high: return Priority::High; - case executor_priority::medium: return Priority::Default; - case executor_priority::low: return Priority::Low; - default: return Priority::Default; - } -} - -/// @brief Asynchronously invoke f on the default_executor. +/// @brief Asynchronously invoke f on the rust-backed executor. /// @tparam F function object type /// @param f a function object. /// @return the result of calling `execute`. @@ -34,45 +21,20 @@ auto enqueue(F f) { }); } -/// @brief Asynchronously invoke `f` on the default_executor with priority `p`. +/// @brief Asynchronously invoke `f` on the rust-backed executor with priority `p`. /// @tparam F function object type /// @param f a function object. /// @param priority /// @return the value returned by `execute_priority`. template <class F, class = std::enable_if_t<std::is_invocable_r< void, F >::value> > -auto enqueue_priority(F f, executor_priority p) { +auto enqueue_priority(F f, Priority p) { return execute_priority(new F(std::move(f)), [](void* f_) { auto f = static_cast<F*>(f_); try { (*f)(); } catch (...) {} delete f; - }, bridge_priority(p)); + }, p); } -/// @brief A thin invokable wrapper around `enqueue_priority`. -/// @tparam Priority the priority at which all given function objects will be enqueued. -template <executor_priority Priority> -struct executor_type { - using result_type = void; - - /// @brief Enqueues the given task on the default_executor with this object's Priority value. - /// @tparam F function object type - /// @param f function object - template <class F> - void operator()(F&& f) const { - enqueue_priority(std::forward<F>(f), Priority); - } -}; - -} // namespace detail - -/// @brief An executor for low priority tasks, enqueued with the call operator. -constexpr auto low_executor = detail::executor_type<detail::executor_priority::low>{}; -/// @brief An executor for standard priority tasks, enqueued with the call operator. -constexpr auto default_executor = detail::executor_type<detail::executor_priority::medium>{}; -/// @brief An executor for high priority tasks, enqueued with the call operator. -constexpr auto high_executor = detail::executor_type<detail::executor_priority::high>{}; - -} // inline namespace v1 -} // namespace stlab +} \ No newline at end of file diff --git a/rustport/src/stlab/mod.rs b/rustport/src/stlab/mod.rs index 14caee35..eb9b13ba 100644 --- a/rustport/src/stlab/mod.rs +++ b/rustport/src/stlab/mod.rs @@ -142,11 +142,11 @@ impl PriorityTaskSystem { fn try_pop(queues: &Vec<NotificationQueue<Task>>, starting_at: usize, modulo: usize) -> Option<Task> { for i in (starting_at..starting_at+modulo).map(|i| i % modulo) { match queues.get(i).unwrap().try_pop() { - Some(t) => Some(t), + Some(t) => { return Some(t) } None => continue }; } - None + return None; } } diff --git a/stlab/concurrency/default_executor.hpp b/stlab/concurrency/default_executor.hpp index 8edd3fd0..5b3f9080 100644 --- a/stlab/concurrency/default_executor.hpp +++ b/stlab/concurrency/default_executor.hpp @@ -11,12 +11,6 @@ #include <stlab/config.hpp> -#if STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) - -#include "bindings.hpp" - -#else - #include <stlab/pre_exit.hpp> #include <stlab/concurrency/set_current_thread_name.hpp> @@ -39,6 +33,8 @@ #include <condition_variable> #include <thread> #include <vector> +#elif STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) +#include "bindings.hpp" #endif /**************************************************************************************************/ @@ -491,6 +487,36 @@ struct executor_type { /**************************************************************************************************/ +#if STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) + +inline auto bridge_priority(executor_priority p) -> Priority { + switch (p) { + case executor_priority::high: return Priority::High; + case executor_priority::medium: return Priority::Default; + case executor_priority::low: return Priority::Low; + default: return Priority::Default; + } +} + +/// @brief A thin invokable wrapper around `enqueue_priority`. +/// @tparam Priority the priority at which all given function objects will be enqueued. +template <executor_priority Priority> +struct executor_type { + using result_type = void; + + /// @brief Enqueues the given task on the default_executor with this object's Priority value. + /// @tparam F function object type + /// @param f function object + template <class F> + void operator()(F&& f) const { + rust::enqueue_priority(std::forward<F>(f), bridge_priority(Priority)); + } +}; + +#endif // STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) + +/**************************************************************************************************/ + } // namespace detail /**************************************************************************************************/ @@ -510,8 +536,6 @@ constexpr auto high_executor = detail::executor_type<detail::executor_priority:: /**************************************************************************************************/ -#endif // STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) - #endif // STLAB_CONCURRENCY_DEFAULT_EXECUTOR_HPP /**************************************************************************************************/ From 47de5dbea5e8c9fc1e67a34139e6ac6dda787f76 Mon Sep 17 00:00:00 2001 From: nickpdemarco <nickpdemarco@gmail.com> Date: Tue, 28 Nov 2023 18:35:15 -0500 Subject: [PATCH 09/13] Run rustfmt --- rustport/cppshim/include/bindings.hpp | 2 +- rustport/src/lib.rs | 48 ++++++------- rustport/src/stlab/coarse_priority_queue.rs | 45 ++++++------ rustport/src/stlab/drop_join_thread_pool.rs | 16 ++--- rustport/src/stlab/mod.rs | 80 +++++++++++++-------- rustport/src/stlab/notification_queue.rs | 17 +++-- rustport/src/stlab/waiter.rs | 9 ++- 7 files changed, 120 insertions(+), 97 deletions(-) diff --git a/rustport/cppshim/include/bindings.hpp b/rustport/cppshim/include/bindings.hpp index f6b88b30..98f8358c 100644 --- a/rustport/cppshim/include/bindings.hpp +++ b/rustport/cppshim/include/bindings.hpp @@ -37,4 +37,4 @@ auto enqueue_priority(F f, Priority p) { }, p); } -} \ No newline at end of file +} diff --git a/rustport/src/lib.rs b/rustport/src/lib.rs index f09e979c..41b1c1e5 100644 --- a/rustport/src/lib.rs +++ b/rustport/src/lib.rs @@ -1,46 +1,46 @@ -use std::{ffi::c_void, sync::Mutex}; use once_cell::sync::Lazy; +use std::{ffi::c_void, sync::Mutex}; -use stlab::{PriorityTaskSystem, Priority}; +use stlab::{Priority, PriorityTaskSystem}; mod stlab; -static TASK_SYSTEM: Lazy<Mutex<PriorityTaskSystem>> = Lazy::new(|| Mutex::new(PriorityTaskSystem::new()) ); +static TASK_SYSTEM: Lazy<Mutex<PriorityTaskSystem>> = + Lazy::new(|| Mutex::new(PriorityTaskSystem::new())); // "Threadsafe" is not a guarantee, it is a requirement. These pointers are assumed to be able to // be sent to another thread, and therefore must not rely on thread-local state. struct ThreadsafeCFnWrapper { - context: *mut c_void, - fn_ptr: extern fn(*mut c_void) + context: *mut c_void, + fn_ptr: extern "C" fn(*mut c_void), } impl ThreadsafeCFnWrapper { - pub(crate) fn new(context: *mut c_void, fn_ptr: extern fn(*mut c_void)) -> Self { - Self { - context, - fn_ptr - } - } - - pub(crate) fn call(&self) { - (self.fn_ptr)(self.context) - } + pub(crate) fn new(context: *mut c_void, fn_ptr: extern "C" fn(*mut c_void)) -> Self { + Self { context, fn_ptr } + } + + pub(crate) fn call(&self) { + (self.fn_ptr)(self.context) + } } unsafe impl Send for ThreadsafeCFnWrapper {} /// Enqueues a the execution of `f(context)` on the PriorityTaskSystem. #[no_mangle] -pub extern "C" fn execute(context: *mut c_void, fn_ptr: extern fn(*mut c_void)) -> i32 { - execute_priority(context, fn_ptr, Priority::Default); - 0 +pub extern "C" fn execute(context: *mut c_void, fn_ptr: extern "C" fn(*mut c_void)) -> i32 { + execute_priority(context, fn_ptr, Priority::Default); + 0 } /// Enqueues a the execution of `f(context)` on the PriorityTaskSystem at the given `priority`. #[no_mangle] -pub extern "C" fn execute_priority(context: *mut c_void, fn_ptr: extern fn(*mut c_void), p: Priority) -> i32 { - let wrap = ThreadsafeCFnWrapper::new(context, fn_ptr); - TASK_SYSTEM.lock().unwrap().execute(move||{ - wrap.call() - }, p); - 0 +pub extern "C" fn execute_priority( + context: *mut c_void, + fn_ptr: extern "C" fn(*mut c_void), + p: Priority, +) -> i32 { + let wrap = ThreadsafeCFnWrapper::new(context, fn_ptr); + TASK_SYSTEM.lock().unwrap().execute(move || wrap.call(), p); + 0 } diff --git a/rustport/src/stlab/coarse_priority_queue.rs b/rustport/src/stlab/coarse_priority_queue.rs index a14166ab..81327fcc 100644 --- a/rustport/src/stlab/coarse_priority_queue.rs +++ b/rustport/src/stlab/coarse_priority_queue.rs @@ -6,12 +6,12 @@ pub struct CoarsePriorityQueue<T> { count: usize, } -impl <T> CoarsePriorityQueue<T> { +impl<T> CoarsePriorityQueue<T> { /// Creates a new, empty `CoarsePriorityQueue`. pub fn new() -> Self { Self { inner: std::collections::BinaryHeap::new(), - count: 0 + count: 0, } } @@ -22,14 +22,15 @@ impl <T> CoarsePriorityQueue<T> { /// Removes the greatest item from the queue at returns it, or None if it is empty. pub fn pop(&mut self) -> Option<T> { - self.inner.pop().and_then(|prioritized| { - Some(prioritized.take_inner()) - }) + self.inner + .pop() + .and_then(|prioritized| Some(prioritized.take_inner())) } /// Pushes an item onto the queue at the given `priority`. pub fn push(&mut self, item: T, priority: Priority) { - self.inner.push(Prioritized::new(item, priority, self.count)); + self.inner + .push(Prioritized::new(item, priority, self.count)); self.count += 1; } } @@ -38,9 +39,9 @@ impl <T> CoarsePriorityQueue<T> { #[derive(Eq, PartialEq, Copy, Clone)] #[repr(C)] pub enum Priority { - Low, - Default, - High + Low, + Default, + High, } /// Use of `Priority` and `Prioritized` requires the client maintain a nondecreasing count which @@ -58,7 +59,7 @@ impl Priority { match self { Priority::Low => 0 << usize::BITS - 2, Priority::Default => 1 << usize::BITS - 2, - Priority::High => 2 << usize::BITS - 2 + Priority::High => 2 << usize::BITS - 2, } } @@ -70,22 +71,24 @@ impl Priority { impl PartialOrd for Priority { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - self.highbit_mask().partial_cmp(&other.highbit_mask()) + self.highbit_mask().partial_cmp(&other.highbit_mask()) } } -/// Pairs an instance of `T` with a `Priority`. +/// Pairs an instance of `T` with a `Priority`. /// Equality and ordering of a Prioritized<T> only considers `priority`, and disregards `inner`. struct Prioritized<T> { inner: T, - priority: usize + priority: usize, } -impl <T> Prioritized<T> { - +impl<T> Prioritized<T> { /// Precondition: count must be less than 2,305,843,009,213,693,951. pub fn new(inner: T, priority: Priority, count: usize) -> Self { - Prioritized { inner, priority: priority.merge_priority_count(count) } + Prioritized { + inner, + priority: priority.merge_priority_count(count), + } } pub fn take_inner(self) -> T { @@ -93,25 +96,25 @@ impl <T> Prioritized<T> { } } -impl<T> PartialEq for Prioritized <T> { +impl<T> PartialEq for Prioritized<T> { #[inline] fn eq(&self, other: &Self) -> bool { self.priority == other.priority } } -impl <T> Eq for Prioritized <T> {} +impl<T> Eq for Prioritized<T> {} -impl<T> PartialOrd for Prioritized <T> { +impl<T> PartialOrd for Prioritized<T> { #[inline] fn partial_cmp(&self, other: &Self) -> Option<Ordering> { self.priority.partial_cmp(&other.priority) } } -impl<T> Ord for Prioritized <T> { +impl<T> Ord for Prioritized<T> { #[inline] fn cmp(&self, other: &Self) -> Ordering { self.priority.cmp(&other.priority) } -} \ No newline at end of file +} diff --git a/rustport/src/stlab/drop_join_thread_pool.rs b/rustport/src/stlab/drop_join_thread_pool.rs index 5e271b61..7f9161af 100644 --- a/rustport/src/stlab/drop_join_thread_pool.rs +++ b/rustport/src/stlab/drop_join_thread_pool.rs @@ -1,4 +1,4 @@ -use std::{thread::JoinHandle, io::Write}; +use std::{io::Write, thread::JoinHandle}; /// A thread pool which joins all spawned threads when dropped. /// @@ -16,11 +16,10 @@ pub struct DropJoinThreadPool<T: Default + Send + Sync + 'static> { threads: Vec<JoinHandle<()>>, // SAFETY: We store this pointer as a `*mut` so it can be dropped via Box::from_raw, but // we only ever hand out immutable references to the pointee. - data: *mut T + data: *mut T, } -impl <T: Default + Send + Sync + 'static> Drop for DropJoinThreadPool <T> { - +impl<T: Default + Send + Sync + 'static> Drop for DropJoinThreadPool<T> { /// Join all spawned threads, and drop `data` manually. fn drop(&mut self) { for thread in std::mem::take(&mut self.threads) { @@ -33,14 +32,13 @@ impl <T: Default + Send + Sync + 'static> Drop for DropJoinThreadPool <T> { } } -impl <T: Default + Send + Sync + 'static> DropJoinThreadPool <T> { - +impl<T: Default + Send + Sync + 'static> DropJoinThreadPool<T> { /// Creates a new `DropJoinThreadPool` with a maximum thread capacity of `thread_limit`, moving /// `data` onto the heap. pub fn new(thread_limit: usize, data: T) -> Self { Self { threads: Vec::<JoinHandle<()>>::with_capacity(thread_limit), - data: Box::into_raw(Box::new(data)) + data: Box::into_raw(Box::new(data)), } } @@ -66,7 +64,7 @@ impl <T: Default + Send + Sync + 'static> DropJoinThreadPool <T> { let f = f.clone(); // SAFETY: We only hand out immutable references to this data. let data: &T = unsafe { &*self.data }; - std::thread::spawn(move||{ + std::thread::spawn(move || { f(i, data); }) })); @@ -98,4 +96,4 @@ impl <T: Default + Send + Sync + 'static> DropJoinThreadPool <T> { let mut stderr = std::io::stderr().lock(); let _ = stderr.write_all(buf); } -} \ No newline at end of file +} diff --git a/rustport/src/stlab/mod.rs b/rustport/src/stlab/mod.rs index eb9b13ba..f8ce7ed5 100644 --- a/rustport/src/stlab/mod.rs +++ b/rustport/src/stlab/mod.rs @@ -1,7 +1,7 @@ use std::cmp::max; use std::num::NonZeroUsize; -use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering as MemoryOrdering}; +use std::sync::Arc; mod coarse_priority_queue; pub use coarse_priority_queue::Priority; @@ -16,7 +16,7 @@ pub mod notification_queue; use notification_queue::NotificationQueue; /// A type-erased, heap-allocated function object. -pub type Task = Box<dyn FnOnce()->() + Send>; +pub type Task = Box<dyn FnOnce() -> () + Send>; /// A portable work-stealing task scheduler with three priorities. /// @@ -56,31 +56,42 @@ impl PriorityTaskSystem { /// Creates a new PriorityTaskSystem. pub fn new() -> Self { // SAFETY: We know 1 is not 0. - let nonzero_available_parallelism = std::thread::available_parallelism().unwrap_or(unsafe { NonZeroUsize::new_unchecked(1) }); + let nonzero_available_parallelism = std::thread::available_parallelism() + .unwrap_or(unsafe { NonZeroUsize::new_unchecked(1) }); let available_parallelism = max(usize::from(nonzero_available_parallelism), 2) - 1; let thread_limit = max(9, available_parallelism * 4 + 1); - let queues = (0..available_parallelism).map(|_| { NotificationQueue::default() }).collect(); + let queues = (0..available_parallelism) + .map(|_| NotificationQueue::default()) + .collect(); let mut pool = DropJoinThreadPool::new(thread_limit, queues); - pool.spawn_n(move |i, queues| { - loop { + + pool.spawn_n( + move |i, queues| loop { let mut task = Self::try_pop(queues, i, available_parallelism); if task.is_none() { let done: bool; (done, task) = queues.get(i).unwrap().pop(); - if done { break } + if done { + break; + } } if task.is_some() { task.unwrap()(); } - } - }, available_parallelism); + }, + available_parallelism, + ); Self { pool, - waiters: Arc::new((0..(thread_limit - available_parallelism)).map(|_| { Waiter::default() }).collect()), + waiters: Arc::new( + (0..(thread_limit - available_parallelism)) + .map(|_| Waiter::default()) + .collect(), + ), index: AtomicUsize::new(0), available_parallelism, } @@ -89,7 +100,10 @@ impl PriorityTaskSystem { /// Push `f` to the first queue in `queues` whose mutex is not under contention. /// If no such queue is found after a single pass, blockingly push `f` to one queue. // REVIEW: I'm not sure `execute` is a good name. I think we want `push`, or `push_with_priority`. - pub fn execute<F>(&self, f: F, p: Priority) where F: FnOnce() -> () + Send + 'static { + pub fn execute<F>(&self, f: F, p: Priority) + where + F: FnOnce() -> () + Send + 'static, + { self.execute_task(Box::new(f), p) } @@ -100,9 +114,11 @@ impl PriorityTaskSystem { let n = self.available_parallelism; // Attempt to push to a queue without blocking, starting with ours. - for i in (i..i+n).map(|i| i % n) { + for i in (i..i + n).map(|i| i % n) { task = queues.get(i).unwrap().try_push(task.unwrap(), priority); - if task.is_none() { return } // An empty return means push was successful. + if task.is_none() { + return; + } // An empty return means push was successful. } // Otherwise, attempt to push to our queue, with blocking. @@ -114,7 +130,7 @@ impl PriorityTaskSystem { pub fn add_thread(&mut self) { let waiters = self.waiters.clone(); let n = self.available_parallelism; - self.pool.spawn(move |i, queues|{ + self.pool.spawn(move |i, queues| { loop { if let Some(task) = Self::try_pop(queues, i, n) { task(); @@ -122,33 +138,41 @@ impl PriorityTaskSystem { } // Note: The following means multiple threads may wait on a single `Waiter`. - if waiters[i - n].wait() { break; } - } + if waiters[i - n].wait() { + break; + } + } }); } // Returns true if any thread was woken. pub fn wake(&self) -> bool { - let any_queue_woken = self.pool.execute_immediately(|queues| { - return queues.iter().any(|queue| queue.wake()) - }); - - if any_queue_woken { true } - else { self.waiters.iter().any(|waiter| waiter.wake()) } + let any_queue_woken = self + .pool + .execute_immediately(|queues| return queues.iter().any(|queue| queue.wake())); + + if any_queue_woken { + true + } else { + self.waiters.iter().any(|waiter| waiter.wake()) + } } /// Attempt to non-blockingly pop a task from each queue in the system, starting at index /// `starting_at`. - fn try_pop(queues: &Vec<NotificationQueue<Task>>, starting_at: usize, modulo: usize) -> Option<Task> { - for i in (starting_at..starting_at+modulo).map(|i| i % modulo) { + fn try_pop( + queues: &Vec<NotificationQueue<Task>>, + starting_at: usize, + modulo: usize, + ) -> Option<Task> { + for i in (starting_at..starting_at + modulo).map(|i| i % modulo) { match queues.get(i).unwrap().try_pop() { - Some(t) => { return Some(t) } - None => continue + Some(t) => return Some(t), + None => continue, }; } return None; } - } -unsafe impl Send for PriorityTaskSystem {} \ No newline at end of file +unsafe impl Send for PriorityTaskSystem {} diff --git a/rustport/src/stlab/notification_queue.rs b/rustport/src/stlab/notification_queue.rs index c9807ee5..2773f1ae 100644 --- a/rustport/src/stlab/notification_queue.rs +++ b/rustport/src/stlab/notification_queue.rs @@ -1,4 +1,4 @@ -use crate::stlab::coarse_priority_queue::{Priority, CoarsePriorityQueue}; +use crate::stlab::coarse_priority_queue::{CoarsePriorityQueue, Priority}; /// A threadsafe priority queue. pub struct NotificationQueue<T> { @@ -16,13 +16,12 @@ impl<T> std::default::Default for NotificationQueue<T> { } impl<T> NotificationQueue<T> { - /// Try to pop from the queue without blocking. /// Returns `None` if our mutex is already locked or if the queue is empty. pub fn try_pop(&self) -> Option<T> { if let Ok(ref mut this) = self.protected.try_lock() { return this.queue.pop(); - } + } return None; } @@ -49,7 +48,7 @@ impl<T> NotificationQueue<T> { this.waiting = false; if this.queue.is_empty() { return (this.done, None); - } + } return (false, this.queue.pop()); } @@ -70,7 +69,7 @@ impl<T> NotificationQueue<T> { } else { return Some(element); } - + // We successfully locked the mutex, did our push, and released the lock. self.ready.notify_one(); return None; @@ -87,11 +86,11 @@ impl<T> NotificationQueue<T> { } } -unsafe impl <T> Send for NotificationQueue<T> {} -unsafe impl <T> Sync for NotificationQueue<T> {} +unsafe impl<T> Send for NotificationQueue<T> {} +unsafe impl<T> Sync for NotificationQueue<T> {} /// The fields of `NotificationQueue` which must be protected by a `Mutex`. -struct NotificationQueueProtectedData <T> { +struct NotificationQueueProtectedData<T> { queue: CoarsePriorityQueue<T>, done: bool, waiting: bool, @@ -102,7 +101,7 @@ impl<T> std::default::Default for NotificationQueueProtectedData<T> { Self { queue: CoarsePriorityQueue::new(), done: false, - waiting: false + waiting: false, } } } diff --git a/rustport/src/stlab/waiter.rs b/rustport/src/stlab/waiter.rs index 30712389..844e7683 100644 --- a/rustport/src/stlab/waiter.rs +++ b/rustport/src/stlab/waiter.rs @@ -10,20 +10,19 @@ pub struct Waiter { impl Default for Waiter { fn default() -> Self { - Waiter::new() + Waiter::new() } } impl Waiter { - - /// Constructs a new Waiter, with `waiting` and `done` set to `false`. + /// Constructs a new Waiter, with `waiting` and `done` set to `false`. pub fn new() -> Self { Self { protected: Mutex::new(WaiterProtectedData { waiting: false, done: false, }), - ready: std::sync::Condvar::new() + ready: std::sync::Condvar::new(), } } @@ -72,4 +71,4 @@ unsafe impl Sync for Waiter {} struct WaiterProtectedData { waiting: bool, done: bool, -} \ No newline at end of file +} From 9823b9116dcfbedbeb6154ddbf5c46430a04cb8a Mon Sep 17 00:00:00 2001 From: nickpdemarco <nickpdemarco@gmail.com> Date: Tue, 28 Nov 2023 18:38:42 -0500 Subject: [PATCH 10/13] Run cargo update --- CMakeLists.txt | 2 +- rustport/Cargo.lock | 126 +++++++++++++++++++++++++++++++++----------- 2 files changed, 97 insertions(+), 31 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 70d90da8..f6730a85 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -118,7 +118,7 @@ if ( BUILD_TESTING ) # add_library( testing INTERFACE ) add_library( stlab::testing ALIAS testing ) - + # # CMake targets linking to the stlab::testing target will (transitively) # link to the Boost::unit_test_framework and to stlab::stlab target. diff --git a/rustport/Cargo.lock b/rustport/Cargo.lock index e97cef7c..2a4c4733 100644 --- a/rustport/Cargo.lock +++ b/rustport/Cargo.lock @@ -90,12 +90,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -143,15 +143,15 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "libc" -version = "0.2.149" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "log" @@ -173,9 +173,9 @@ checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] @@ -200,15 +200,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -219,22 +219,22 @@ checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "serde" -version = "1.0.190" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.190" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -267,9 +267,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -286,14 +286,14 @@ dependencies = [ "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] @@ -356,7 +356,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -365,13 +374,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -380,38 +404,80 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" From ac8e6920ef409d975deffea0127253219c6842c5 Mon Sep 17 00:00:00 2001 From: nickpdemarco <nickpdemarco@gmail.com> Date: Tue, 28 Nov 2023 18:51:50 -0500 Subject: [PATCH 11/13] Add propagating panic to threadpool destructor --- rustport/src/stlab/drop_join_thread_pool.rs | 9 ++++++++- rustport/src/stlab/mod.rs | 8 ++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/rustport/src/stlab/drop_join_thread_pool.rs b/rustport/src/stlab/drop_join_thread_pool.rs index 7f9161af..cf461e29 100644 --- a/rustport/src/stlab/drop_join_thread_pool.rs +++ b/rustport/src/stlab/drop_join_thread_pool.rs @@ -6,6 +6,8 @@ use std::{io::Write, thread::JoinHandle}; /// threads. As such, it must be Send + Sync, and 'static (having no non-static references). When /// tasks in this pool are spawned, they are passed an immutable reference to said object (there is /// no way to acquire a mutable reference). +/// +/// If a thread panics, the panic will be propagated while dropping this pool. /// /// Note: it remains to be seen if it is possible (or useful) to loosen the 'static requirement here /// to instead allow any lifetimes which do not outlive this pool. Early attempts at this polluted @@ -21,9 +23,14 @@ pub struct DropJoinThreadPool<T: Default + Send + Sync + 'static> { impl<T: Default + Send + Sync + 'static> Drop for DropJoinThreadPool<T> { /// Join all spawned threads, and drop `data` manually. + /// + /// If a thread in the pool paniced, that panic will propagate here. fn drop(&mut self) { for thread in std::mem::take(&mut self.threads) { - let _ = thread.join(); // TODO handle error? what's the rule for drop? + match thread.join() { + Ok(..) => continue, + Err(e) => std::panic::resume_unwind(e) + } } // SAFETY: We only call from_raw once, in this `drop`. We do not permit copies of this diff --git a/rustport/src/stlab/mod.rs b/rustport/src/stlab/mod.rs index f8ce7ed5..9865fa24 100644 --- a/rustport/src/stlab/mod.rs +++ b/rustport/src/stlab/mod.rs @@ -20,10 +20,10 @@ pub type Task = Box<dyn FnOnce() -> () + Send>; /// A portable work-stealing task scheduler with three priorities. /// -/// By default, this scheduler spins up a number of threads corresponding to the amount of -/// parallelism available on the target platform, namely, std::thread::available_parallelism() - 1. -/// Each thread is assigned a threadsafe priority queue. To reduce contention on push and pop -/// operations, a thread will first attempt to acquire the lock for its own queue without blocking. +/// This scheduler spins up a number of threads corresponding to the amount of parallelism available +/// on the target platform, namely, std::thread::available_parallelism() - 1. Each thread is +/// assigned a threadsafe priority queue. To reduce contention on push and pop operations, a thread +/// will first attempt to acquire the lock for its own queue without blocking. /// If that fails, it will attempt the same non-blocking push/pop for each other priority queue in /// the scheduler. Finally, if each of those attempts also fail, the thread will attempt a blocking /// push/pop on its own priority queue. From 4f5362e9f65ff9ada04ef5d6bc13064d684085fa Mon Sep 17 00:00:00 2001 From: nickpdemarco <nickpdemarco@gmail.com> Date: Wed, 29 Nov 2023 12:19:42 -0500 Subject: [PATCH 12/13] Better contracts --- rustport/src/lib.rs | 14 ++++++++++++-- rustport/src/stlab/mod.rs | 1 - rustport/src/stlab/waiter.rs | 3 +-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/rustport/src/lib.rs b/rustport/src/lib.rs index 41b1c1e5..ee2e377a 100644 --- a/rustport/src/lib.rs +++ b/rustport/src/lib.rs @@ -4,11 +4,15 @@ use std::{ffi::c_void, sync::Mutex}; use stlab::{Priority, PriorityTaskSystem}; mod stlab; +/// A static instance of the task system which is invoked through the `execute` functions below. static TASK_SYSTEM: Lazy<Mutex<PriorityTaskSystem>> = Lazy::new(|| Mutex::new(PriorityTaskSystem::new())); -// "Threadsafe" is not a guarantee, it is a requirement. These pointers are assumed to be able to -// be sent to another thread, and therefore must not rely on thread-local state. + +/// A function pointer paired with a context, akin to a C++ lambda and its captures. +/// +/// "Threadsafe" is not a guarantee, it is a requirement. These pointers are assumed to be able to +/// be sent to another thread, and therefore must not rely on thread-local state. struct ThreadsafeCFnWrapper { context: *mut c_void, fn_ptr: extern "C" fn(*mut c_void), @@ -19,14 +23,18 @@ impl ThreadsafeCFnWrapper { Self { context, fn_ptr } } + // Note: there is no way in stable rust to make a struct invocable with () syntax. pub(crate) fn call(&self) { (self.fn_ptr)(self.context) } } +/// `ThreadsafeCFnWrapper` may not rely on thread-local state. unsafe impl Send for ThreadsafeCFnWrapper {} /// Enqueues a the execution of `f(context)` on the PriorityTaskSystem. +/// +/// Precondition: Neither `context` nor `fn_ptr` may rely on thread-local state. #[no_mangle] pub extern "C" fn execute(context: *mut c_void, fn_ptr: extern "C" fn(*mut c_void)) -> i32 { execute_priority(context, fn_ptr, Priority::Default); @@ -34,6 +42,8 @@ pub extern "C" fn execute(context: *mut c_void, fn_ptr: extern "C" fn(*mut c_voi } /// Enqueues a the execution of `f(context)` on the PriorityTaskSystem at the given `priority`. +/// +/// Precondition: Neither `context` nor `fn_ptr` may rely on thread-local state. #[no_mangle] pub extern "C" fn execute_priority( context: *mut c_void, diff --git a/rustport/src/stlab/mod.rs b/rustport/src/stlab/mod.rs index 9865fa24..e50392e1 100644 --- a/rustport/src/stlab/mod.rs +++ b/rustport/src/stlab/mod.rs @@ -99,7 +99,6 @@ impl PriorityTaskSystem { /// Push `f` to the first queue in `queues` whose mutex is not under contention. /// If no such queue is found after a single pass, blockingly push `f` to one queue. - // REVIEW: I'm not sure `execute` is a good name. I think we want `push`, or `push_with_priority`. pub fn execute<F>(&self, f: F, p: Priority) where F: FnOnce() -> () + Send + 'static, diff --git a/rustport/src/stlab/waiter.rs b/rustport/src/stlab/waiter.rs index 844e7683..9d6c6b06 100644 --- a/rustport/src/stlab/waiter.rs +++ b/rustport/src/stlab/waiter.rs @@ -36,8 +36,7 @@ impl Waiter { } /// Sets waiting to `false`. If waiting was `true`, wake one waiter and return `true`. Otherwise, return `false`. - /// If `try_lock` fails, return `false`. (REVIEW: why?) - /// (REVIEW: is it redundant to express that `waiting` and `done` are accesed under a mutex?) + /// If `try_lock` fails, return `false`. pub fn wake(&self) -> bool { if let Ok(ref mut this) = self.protected.try_lock() { if !this.waiting { From 19505c6ce87a47b9bbf4d2e23ecea76f5c0d4f1b Mon Sep 17 00:00:00 2001 From: nickpdemarco <nickpdemarco@gmail.com> Date: Thu, 30 Nov 2023 14:49:26 -0500 Subject: [PATCH 13/13] Clang 15, improve a comment --- .github/matrix.json | 4 ++-- rustport/src/stlab/mod.rs | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/matrix.json b/.github/matrix.json index c32dc4d7..883e8af1 100644 --- a/.github/matrix.json +++ b/.github/matrix.json @@ -7,9 +7,9 @@ "os": "ubuntu-22.04" }, { - "name": "Linux Clang 14", + "name": "Linux Clang 15", "compiler": "clang", - "version": "14", + "version": "15", "os": "ubuntu-22.04" }, { diff --git a/rustport/src/stlab/mod.rs b/rustport/src/stlab/mod.rs index e50392e1..b868d1b7 100644 --- a/rustport/src/stlab/mod.rs +++ b/rustport/src/stlab/mod.rs @@ -106,13 +106,15 @@ impl PriorityTaskSystem { self.execute_task(Box::new(f), p) } + /// Push `task` to the first queue in `queues` whose mutex is not under contention. + /// If no such queue is found after a single pass, blockingly push `task` to one queue. pub fn execute_task(&self, task: Task, priority: Priority) { self.pool.execute_immediately(|queues| { let mut task: Option<Task> = Some(task); let i = self.index.fetch_add(1, MemoryOrdering::SeqCst); let n = self.available_parallelism; - // Attempt to push to a queue without blocking, starting with ours. + // Attempt to push to each queue without blocking. for i in (i..i + n).map(|i| i % n) { task = queues.get(i).unwrap().try_push(task.unwrap(), priority); if task.is_none() { @@ -120,7 +122,7 @@ impl PriorityTaskSystem { } // An empty return means push was successful. } - // Otherwise, attempt to push to our queue, with blocking. + // Otherwise, attempt to blockingly push to one queue. queues.get(i % n).unwrap().push(task.unwrap(), priority); }); }