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:
-
Both read and write access needs to be serialized to satisfy the Rust ownership
policies. We do that with a Mutex.
-
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
-
string conversions
-
cast only for primitives, else From and Into
-
Traits - see donovan pain-points.html, section 10
-
HashMap entry api (donovan again)
-
Reading values of Move types from Arrays and Vectors
- Living well with the ownership rules - pass-by-reference functions
- ...