ib RustBite Hacks & Helpers
about
05/19/2022
RustBites - Hacks & Helpers
Rust Bites Code

Rust Bite - Hacks & Helpers

ironing out wrinkles in tricky code - tricky, of course, is in the eye of the beholder

This Bite presents techniques for solving code problems that I encountered or which appear often in QA forums. Of course there are many of those. I've selected ones that I thought were interesting. I expect to add to this page continuously as I work on Rust-based projects.

1.0  Struct with method hosting thread

Asynchronous processing often needs to share some mutable data with the thread host. For a method, that usually means sharing member data of a host object. But that needs to be constructed carefully so that:
  1. Both read and write access needs to be serialized to satisfy the Rust ownership policies. We do that with a Mutex.
  2. The construction needs to guarantee that the thread's reference won't become invalid if the thread outlives the host object. We do that by using an Arc, a thread-safe reference-counted pointer. The Arc stores its content on the heap, returning a reference to that. Any other code that needs access to the Arc's data calls Arc::clone(&original). Now, the content will live until every reference to it has been dropped.
Compiler error messages give a lot of help: lifetime errors are usually problems handling the Arc correctly. Move errors are often attempting to use a value instead of a reference in the thread. You will find code for all examples below in RustBiteByByte/hacks_and_helps/methods_with_threads

1.1 - User-defined Type with thread start() method

This example is a simple demo that sums a loop index in its thread closure. This same technique was used in the RustComm library that does something much more useful; it uses message passing to communicate between two endpoints using std::net::TcpStream and std::net::TcpListener.
Test1 - User-defined type with thread start() use std::thread::{JoinHandle}; use std::sync::{Arc, Mutex}; struct Test1 { count: Arc<Mutex<i32>>, } impl Test1 { fn new() -> Test1 { Test1 { count: Arc::new(Mutex::new(0)), } } fn start(&mut self) -> JoinHandle<()> { /* get a shared pointer */ let local_count = self.count.clone(); std::thread::spawn(move || { for i in 0..5 { let mut data = local_count.lock().unwrap(); *data += i; print!("\n {:?}", data); } }) } fn show_count(&self) { print!( "\n\n t1 result = {:?}", self.count.lock().unwrap() ); } } Using Code: fn main() { print!("\n -- demo Test1 --"); let mut t1 = Test1::new(); let handle = t1.start(); /* do some useful work here */ let _ = handle.join(); t1.show_count(); println!(); } Output: -- demo Test1 -- 0 1 3 6 10 t1 result = 10

1.2  User-defined Type with thread starting in constructor new()

The code below illustrates how to start a thread in the type's constructor, new(). That would be appropriate if an instance's functionality depended on the thread running.
Test3 - thread starting in new() struct Test3 { counter: Arc<Mutex<i32>>, } impl Test3 { /*----------------------------------------------------- This example uses the same technique as Test1, but moves the thread into new(), the Test3 constructor. That would be a good idea for any type whose instances need the running thread to operate as expected. Note that new() now returns a tuple with the newly constructed Test2 instance and the thread handle. Look at main to see how that is used. */ fn new() -> (Test3, JoinHandle<()>) { /* create mutex guarded i32 in heap memory */ let scount = Arc::new(Mutex::new(0)); /* create shared reference to that value */ let share = Arc::clone(&scount); /* scount is moved into thread's closure */ let handle = std::thread::spawn(move || { for i in 0..5 { let mut data = scount.lock().unwrap(); *data += i; print!("\n {:?}", data); } }); /* share holds a valid ref to guarded i32 in heap */ ( Test3 { counter: share, }, handle ) } fn show_count(&self) { print!( "\n\n t3 result = {:?}", self.counter.lock().unwrap() ); } } Using Code: fn main() { print!("\n -- demo Test3 --"); let (t3, handle) = Test3::new(); /* do some useful work here */ let _ = handle.join(); t3.show_count(); } Output: -- demo Test3 -- 0 1 3 6 10 t3 result = 10

1.3 - User-defined Type with stop() method

This example illustrates how to stop an asynchrounous method with a user command. The Test4 type provides an AtomicBool to record the user's intent to stop, and a method, stop(), to reset it. The method show_count() displays the number of thread iterations before being stopped by the user.
Test4 - graceful thread stop struct Test4 { counter: Arc<Mutex<i32>>, do_run : Arc<AtomicBool>, } impl Test4 { /*----------------------------------------------------- This example demonstrates graceful thread shutdown using an AtomicBool, set by the user and tested in the thread loop. */ fn new() -> (Test4, JoinHandle<()>) { let scount = Arc::new(Mutex::new(0)); let share = Arc::clone(&scount); let run = Arc::new(AtomicBool::new(true)); let run_clone = Arc::clone(&run); let handle = std::thread::spawn(move || { for i in 0..5000 { if !run.load(Ordering::Relaxed) { break; } /* slow down loop for display */ std::thread::sleep( Duration::from_micros(200) ); let mut data = scount.lock().unwrap(); *data = i; print!("\n {:?}", data); } }); ( Test4 { counter: share, do_run: run_clone }, handle ) } fn stop(&self) { self.do_run.store(false, Ordering::Relaxed); } fn show_count(&self) { print!( "\n\n t4 result = {:?}", self.counter.lock().unwrap() ); } } Using Code: fn main() { print!( "\n -- demo Test4 - graceful stop --" ); let (t4, handle) = Test4::new(); std::thread::sleep( Duration::from_millis(100) ); t4.stop(); let _ = handle.join(); t4.show_count(); } Output: -- demo Test4 - graceful stop -- 0 1 2 3 4 5 6 7 t4 result = 7

2.0 - Dates and Times

This section presents date and time tools for testing and logging.

2.1 - StopWatch

StopWatch is a user-defined type that measures time that elapses between calls to StopWatch::start() and StopWatch::stop(). I've used it to test the performance and throughtput of RustComm.
StopWatch extern crate chrono; /*----------------------------------------------- StopWatch type measures elapsed times */ use std::time::*; use std::{thread}; use std::thread::{JoinHandle}; use std::fmt::*; #[derive(Debug, Clone, Copy)] pub struct StopWatch { start: Instant, elapsed: Duration, } impl StopWatch { pub fn new() -> StopWatch { StopWatch { start: Instant::now(), elapsed: Duration::new(0,0), } } pub fn start(&mut self) { self.start = Instant::now(); } pub fn stop(&mut self) -> Duration { self.elapsed = self.start.elapsed(); self.elapsed } pub fn elapsed_micros(&self) -> u128 { self.elapsed.as_micros() } pub fn elapsed_millis(&self) -> u128 { self.elapsed.as_millis() } pub fn elapsed_secs(&self) -> u64 { self.elapsed.as_secs() } } fn sleep(millisec:u64) { let secs = Duration::from_millis(millisec); thread::sleep(secs); } Using Code: /*----------------------------------------------- stop_watch displays thread sleep time - expect small variations from run to run due to uncertainty in sleep interval */ fn stop_watch(millisec: u64) { let mut sw = StopWatch::new(); sleep(millisec); let time = sw.stop().as_millis(); print!("\n elapsed time = {:?}",time); } fn main() { print!("\n -- demo StopWatch --"); stop_watch(25); println!(); } Output: -- demo StopWatch -- elapsed time = 26

2.2  Timer emits callback

Timer start accepts a time in milliseconds and a closure to run at the end of that time.
Timer /*----------------------------------------------- Timer instance invokes callback after specified time. - callback runs on Timer thread */ #[derive(Debug, Clone, Copy)] struct Timer { start: Instant, elapsed: Duration, } impl Timer { pub fn new(time: u64) -> Timer { Timer { start: Instant::now(), elapsed: Duration::new(time, 0), } } pub fn start<F>(&mut self, time: u64, callback:F) -> JoinHandle<()> where F:FnOnce() + Send + 'static { self.start = Instant::now(); let handle = thread::spawn(move || { let ttw = Duration::from_millis(time); thread::sleep(ttw); callback(); }); handle } } /*----------------------------------------------- Demo Timer, using closure callback */ fn timer(millisec: u64) -> JoinHandle<()> { let mut tmr = Timer::new(millisec); let cl = move || { print!( "\n time's up after {:?} milliseconds", millisec ) }; tmr.start(millisec, cl) } Using Code: fn main() { print!("\n -- demo Timer --"); print!("\n starting timer(200)"); let handle = timer(200); print!( "\n do some work while waiting for timer" ); let _ = handle.join(); println!(); } Output: -- demo Timer -- starting timer(200) do some work while waiting for timer time's up after 200 milliseconds

2.3  Date-Time Stamps

Date-time stamps are used for display, often while logging or recording test data.
Time and Date /*----------------------------------------------- Date-Time stamp */ #[allow(unused_imports)] use chrono::{DateTime, Local, Datelike, Timelike}; pub fn date_time_stamp() -> String { let now: DateTime<Local> = Local::now(); /*--------------------------------------------- format DateTime string using chrono formatting */ let mut now_str = format!("{}", now.to_rfc2822()); /* remove trailing -0400 */ now_str.truncate(now_str.len() - 6); now_str } fn convert_month(m:usize) -> &'static str { let dv = vec![ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; dv[m] } pub fn date_stamp() -> String { let now: DateTime<Local> = Local::now(); /* format date string */ let (_is_common_era, year) = now.year_ce(); let idx:usize = (now.month() - 1) as usize; let mon = convert_month(idx); let now_str = format!( "{:0>2} {} {}", now.day(), mon, year ); now_str } pub fn time_stamp() -> String { let now: DateTime<Local> = Local::now(); /* format time string */ let now_str = format!( "{:0>2}:{:0>2}:{:0>2}", now.hour(), now.minute(), now.second() ); now_str } Using Code: fn main() { print!("\n -- demo DateTimeStamp --"); print!("\n now is: {:?}", date_time_stamp()); print!("\n date is: {:?}", date_stamp()); print!("\n time is: {:?}", time_stamp()); } Output: -- demo DateTimeStamp -- now is: "Sun, 26 Jul 2020 17:39:51" date is: "26 Jul 2020" time is: "17:39:51"

3.0  Coming Soon

  1. string conversions
  2. cast only for primitives, else From and Into
  3. Traits - see donovan pain-points.html, section 10
  4. HashMap entry api (donovan again)
  5. Reading values of Move types from Arrays and Vectors
    • Clone
    • Option::take()
  6. Living well with the ownership rules - pass-by-reference functions
  7. ...
  Next Prev Pages Sections About Keys