From 77a7904e73bcbfbed2e644b1a216b8e18aae14e7 Mon Sep 17 00:00:00 2001 From: msiglreith Date: Tue, 11 Feb 2020 20:04:30 +0100 Subject: [PATCH 1/2] glacier: Initial proposal --- .gitignore | 2 + proposal-glacier/Cargo.toml | 9 ++++ proposal-glacier/README.md | 68 +++++++++++++++++++++++++++++ proposal-glacier/examples/basic.rs | 37 ++++++++++++++++ proposal-glacier/src/lib.rs | 13 ++++++ proposal-glacier/src/thread_pool.rs | 42 ++++++++++++++++++ 6 files changed, 171 insertions(+) create mode 100644 proposal-glacier/Cargo.toml create mode 100644 proposal-glacier/README.md create mode 100644 proposal-glacier/examples/basic.rs create mode 100644 proposal-glacier/src/lib.rs create mode 100644 proposal-glacier/src/thread_pool.rs diff --git a/.gitignore b/.gitignore index 088ba6b..d7c704b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ # Generated by Cargo # will have compiled files and executables /target/ +/proposal-glacier/target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock +/proposal-glacier/Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk diff --git a/proposal-glacier/Cargo.toml b/proposal-glacier/Cargo.toml new file mode 100644 index 0000000..df27226 --- /dev/null +++ b/proposal-glacier/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "proposal-glacier" +version = "0.1.0" +authors = ["msiglreith "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/proposal-glacier/README.md b/proposal-glacier/README.md new file mode 100644 index 0000000..c8cfcd9 --- /dev/null +++ b/proposal-glacier/README.md @@ -0,0 +1,68 @@ +# Summary +[summary]: #summary + +A proposal for thread pool interface based on existing `future` API. + +# Motivation +[motivation]: #motivation + +We are aiming at trying to allow thread pool (or more general `executor`) implementers give maximum freedom to support different execution approaches as used in practice (e.g https://github.com/rust-gamedev/wg/issues/75#issuecomment-564972595) + +On the other hand we need to keep the library authors in mind to provide an appealing API, in particular for user how not primarily care about gamedev at all. + +Important aspects in gamedev: + +- Full control over the thread pool creation and setup +- Support heterogeneous workloads (e.g IO tasks <-> high priority) + +# Explanation +[explanation]: #explanation + +Let's look at a simplified version of the `future` core API: +```rust +pub trait Executor { + /// Schedule a new task for execution. + fn spawn(&mut self, f: impl Task); +} + +pub trait Task : Send + Sync + 'static { + /// Execution of the task. + fn poll(&mut self); +} +``` + +As library author, who 'designs' tasks, we would have to take an `Executor`. +The library consumer on the other hand needs to provide the corresponding executor when calling into the library. + +#### "Full control over the thread pool creation and setup" + +Given the above interface, we don't limit executor implementers in anyway regarding thread number, core pinning, priority or supporting more complex setups (e.g fibers, groups of threads, ..). + +#### "Support heterogeneous workloads (e.g IO tasks <-> high priority)" + +The example below should indicate how heterogeneous workloads can be handled. +This is reponsibility of the caller, the library author has no control over this! + +```rust +fn important_work(executor: &mut impl Executor, desc: &str) { + executor.spawn(Task { val: 10, desc: desc.into() }); +} + +fn heavy_work(executor: &mut impl Executor, desc: &str) { + executor.spawn(Task { val: 1000, desc: desc.into() }); +} + +// ... + +let mut executor = glacier::WorkQueue::new(); + +heavy_work(&mut executor.normal_queue()); +important_work(&mut executor.high_queue()); +``` + +# Drawbacks +[drawbacks]: #drawbacks + +- On a similar note as allocators for examples, we need to explicitly pass the executors around, which bloats the function signature. Other languages like `dyon` (or `jai`?) have builtin support for context parameters providing syntactic sugar (see https://github.com/PistonDevelopers/dyon/issues/224). + +- Futures may have additional overhead, which could be avoided with a more simplistic API. diff --git a/proposal-glacier/examples/basic.rs b/proposal-glacier/examples/basic.rs new file mode 100644 index 0000000..be9d754 --- /dev/null +++ b/proposal-glacier/examples/basic.rs @@ -0,0 +1,37 @@ +use proposal_glacier as glacier; +use glacier::Executor; + +pub struct Task { + val: usize, + desc: String, +} + +impl glacier::Task for Task { + fn poll(&mut self) { + println!("{}: {:?}", &self.desc, self.val); + std::thread::sleep_ms(self.val as _); + } +} + +fn important_work(executor: &mut impl Executor, desc: &str) { + executor.spawn(Task { val: 10, desc: desc.into() }); +} + +fn heavy_work(executor: &mut impl Executor, desc: &str) { + executor.spawn(Task { val: 1000, desc: desc.into() }); +} + +fn main() { + let mut normal_queue = glacier::WorkQueue::new(8); + let mut prio_queue = glacier::WorkQueue::new(2); + + for i in 0..16 { + heavy_work(&mut normal_queue, "heavy..".into()); + } + + for i in 0..4 { + important_work(&mut prio_queue, "!"); + } + + loop { } // do all tasks +} diff --git a/proposal-glacier/src/lib.rs b/proposal-glacier/src/lib.rs new file mode 100644 index 0000000..e2d823c --- /dev/null +++ b/proposal-glacier/src/lib.rs @@ -0,0 +1,13 @@ + +mod thread_pool; +pub use thread_pool::*; + +/// Task executor API comparable to https://doc.rust-lang.org/1.29.2/std/task/trait.Executor.html.WorkQueue +pub trait Executor { + fn spawn(&mut self, f: impl Task); +} + +/// Task trait comparable to `Future`. +pub trait Task : Send + Sync + 'static { + fn poll(&mut self); +} diff --git a/proposal-glacier/src/thread_pool.rs b/proposal-glacier/src/thread_pool.rs new file mode 100644 index 0000000..45621d9 --- /dev/null +++ b/proposal-glacier/src/thread_pool.rs @@ -0,0 +1,42 @@ + +use std::sync::mpsc::{channel, Sender, Receiver}; +use std::sync::{Mutex, Arc}; +use super::*; + +pub struct WorkQueue { + tx: Sender>, + rx: Arc>>>, +} + +impl WorkQueue { + pub fn new(num_threads: usize) -> Self { + let (tx, rx): (Sender>, Receiver>) = channel(); + + let rx = Arc::new(Mutex::new(rx)); + for _ in 0..num_threads { + let rx = Arc::clone(&rx); + std::thread::spawn(move || { + loop { + let job = rx.lock().unwrap().recv(); + match job { + Ok(mut job) => { + job.poll(); + } + Err(..) => break, + } + } + }); + } + + WorkQueue { + tx, + rx, + } + } +} + +impl Executor for WorkQueue { + fn spawn(&mut self, f: impl Task) { + self.tx.send(Box::new(f)).unwrap(); + } +} From 48420f92c12107d3efc3a8db42163a76b6ccefc1 Mon Sep 17 00:00:00 2001 From: msiglreith Date: Wed, 12 Feb 2020 18:47:43 +0100 Subject: [PATCH 2/2] glacier: Add example instructions --- proposal-glacier/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/proposal-glacier/README.md b/proposal-glacier/README.md index c8cfcd9..d451490 100644 --- a/proposal-glacier/README.md +++ b/proposal-glacier/README.md @@ -3,6 +3,12 @@ A proposal for thread pool interface based on existing `future` API. +### Example: basic +Simple example to show the `Executor` trait in action. Uses two threadpools with seperated job queues. +``` +cargo run --example basic +``` + # Motivation [motivation]: #motivation