Threads and Smart Pointers
Now, let's return to the unresolved issue mentioned earlier: I want to share a read-only piece of data, such as a large array, across multiple threads. I can't clone this large array, nor can I move it to just one thread. Based on the logic of smart pointers mentioned earlier, we can accomplish this task using smart pointers. Below is an example:
const TOTAL_SIZE: usize = 100 * 1000; // Array length const NTHREAD: usize = 6; // Number of threads let data: Vec<i32> = (1..(TOTAL_SIZE + 1) as i32).collect(); // Initialize an array from 1 to n let arc_data = Arc::new(data); // Transfer ownership of data to arc_data let result = Arc::new(AtomicU64::new(0)); // Collect results (atomic operations) let mut thread_handlers = vec![]; // Collect thread handles for i in 0..NTHREAD { // Clone Arc to prepare for moving into the thread; increases reference count without deep copying internal data let test_data = arc_data.clone(); let res = result.clone(); thread_handlers.push( thread::spawn(move || { let id = i; // Find the partition for this thread let chunk_size = TOTAL_SIZE / NTHREAD + 1; let start = id * chunk_size; let end = std::cmp::min(start + chunk_size, TOTAL_SIZE); // Calculate the sum let mut sum = 0; for i in start..end { sum += test_data[i]; } // Atomic operation res.fetch_add(sum as u64, Ordering::SeqCst); println!("id={}, sum={}", id, sum); }) ); } // Wait for all threads to finish for th in thread_handlers { th.join().expect("The sender thread panic!!!"); } // Output results println!("result = {}", result.load(Ordering::SeqCst));
The example above uses multiple threads to compute the sum of a large array in parallel, with each thread calculating its own portion. In this code:
Each thread is provided with a read-only array, which we wrapped in an
Arc
smart pointer.Each thread is provided with a variable for collecting data, which we wrapped in
Arc<AtomicU64>
.Note: The objects wrapped by
Arc
are immutable, so if mutability is required, you must wrap them in atomic objects orMutex/Cell
objects.
These measures help address the issue of "sharing after moving ownership in threads."
Polymorphism and Runtime Type Identification
Through Trait Polymorphism
Polymorphism is key to abstraction and decoupling, making it essential for a high-level language to implement it. In C++, polymorphism is achieved through virtual function tables (refer to "C++ Virtual Function Tables"). Rust is similar; however, its programming paradigm resembles Java's interfaces. This is achieved through borrowing from Erlang's trait objects. Here’s the relevant code:
struct Rectangle { width: u32, height: u32, } struct Circle { x: u32, y: u32, radius: u32, } trait IShape { fn area(&self) -> f32; fn to_string(&self) -> String; }
We have two structures, one for a "rectangle" and another for a "circle," along with a trait object IShape
(apologies for the Java naming convention) that includes two methods: area()
for calculating the area and to_string()
for converting to a string. The relevant implementations are as follows:
impl IShape for Rectangle { fn area(&self) -> f32 { (self.height * self.width) as f32 } fn to_string(&self) -> String { format!("Rectangle -> width={} height={} area={}", self.width, self.height, self.area()) } } use std::f64::consts::PI; impl IShape for Circle { fn area(&self) -> f32 { (self.radius * self.radius) as f32 * PI as f32 } fn to_string(&self) -> String { format!("Circle -> x={}, y={}, area={}", self.x, self.y, self.area()) } }
Now, we can utilize polymorphism as follows (using the exclusive smart pointer class Box
):
use std::vec::Vec; let rect = Box::new(Rectangle { width: 4, height: 6 }); let circle = Box::new(Circle { x: 0, y: 0, radius: 5 }); let mut v: Vec<Box<dyn IShape>> = Vec::new(); v.push(rect); v.push(circle); for i in v.iter() { println!("area={}", i.area()); println!("{}", i.to_string()); }
Downcasting
However, in C++, the type of polymorphism is abstract, and if we want to cast it to a concrete type, we refer to this as runtime type identification (RTTI). In C++, this is done using type_id
or dynamic_cast
. In Rust, casting is done using the as
keyword, but this is compile-time identification, not runtime. So, how is this done in Rust?
We can use Rust’s std::any::Any
, which allows us to perform downcasting through the downcast_ref
method. Thus, we need to modify the existing code.
First, we must let IShape
inherit from Any
and add an as_any()
casting interface:
use std::any::Any; trait IShape: Any + 'static { fn as_any(&self) -> &dyn Any; // Other methods... }
Next, implement this interface in the concrete classes:
impl IShape for Rectangle { fn as_any(&self) -> &dyn Any { self } // Other methods... } impl IShape for Circle { fn as_any(&self) -> &dyn Any { self } // Other methods... }
Now we can perform runtime downcasting:
let mut v: Vec<Box<dyn IShape>> = Vec::new(); v.push(rect); v.push(circle); for i in v.iter() { if let Some(s) = i.as_any().downcast_ref::<Rectangle>() { println!("downcast - Rectangle w={}, h={}", s.width, s.height); } else if let Some(s) = i.as_any().downcast_ref::<Circle>() { println!("downcast - Circle x={}, y={}, r={}", s.x, s.y, s.radius); } else { println!("invalid type"); } }
Trait Operator Overloading
Operator overloading is very helpful for generic programming. If all objects can perform comparisons like greater than, less than, or equal to, they can be directly used in standard sorting algorithms. In Rust, operator overloading traits are found under std::ops
, while comparison operator traits are found under std::cmp
. Let’s see an example:
Suppose we have an "Employee" object, and we want to sort employees by their salary. If we want to use the Vec::sort()
method, we need to implement various "comparison" methods for this object. These methods are found in std::cmp
, which includes four traits: Ord
, PartialOrd
, Eq
, and PartialEq
. The Ord
trait relies on PartialOrd
and Eq
, while the Eq
trait depends on PartialEq
. This means you need to implement all these traits. The Eq
trait has no methods, so its implementation is as follows:
use std::cmp::{Ord, PartialOrd, PartialEq, Ordering}; #[derive(Debug)] struct Employee { name: String, salary: i32, } impl Ord for Employee { fn cmp(&self, rhs: &Self) -> Ordering { self.salary.cmp(&rhs.salary) } } impl PartialOrd for Employee { fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> { Some(self.cmp(rhs)) } } impl Eq for Employee {} impl PartialEq for Employee { fn eq(&self, rhs: &Self) -> bool { self.salary == rhs.salary } }
Now, we can perform the following operations:
rust複製程式碼let mut v = vec![ Employee { name: String::from("Bob"), salary: 2048 }, Employee { name: String::from("Alice"), salary: 3208 }, Employee { name: String::from("Tom"), salary: 2359 }, Employee { name: String::from("Jack"), salary: 4865 }, Employee { name: String::from("Marray"), salary: 3743 }, Employee { name: String::from("Hao"), salary: 2964 }, Employee { name: String::from("Chen"), salary: 4197 }, ];// Use a for-loop to find the employee with the highest salarylet mut e = &v[0];for i in 0..v.len() { if *e < v[i] { e = &v[i]; } }println!("max = {:?}", e);// Use standard methodsprintln!("min = {:?}", v.iter().min().unwrap());println!("max = {:?}", v.iter().max().unwrap());// Use standard sorting methodv.sort();println!("{:?}", v);
Summary
Now let's summarize:
In Rust, the most important concepts are "immutability," "ownership," and "traits."
Regarding ownership, Rust prefers to move ownership. If borrowing is needed, references must be used.
Moving ownership can lead to programming complexity, especially when trying to move two variables simultaneously.
The issue of references (borrowing) relates to lifetimes, which sometimes requires programmers to annotate lifetimes.
Ownership complexities arise in functional closures and multithreading scenarios.
Smart pointers can resolve the complexities introduced by ownership and borrowing but bring other issues.
Finally, we introduced Rust's trait objects for achieving polymorphism and function overloading.
Rust is a fairly strict programming language, rigorously checking:
Whether variables are mutable
Whether ownership of variables has been moved
Whether the lifetimes of references are valid
Whether objects need to implement certain traits
These factors can reduce the flexibility of compilation and sometimes necessitate "sugar coating," which may lead to initial discomfort for programmers. The frustration of compilation failures can also be disheartening. When I first learned Rust, I attempted to implement a singly linked list and found it challenging. This indicates that if you don't fully grasp Rust's concepts, writing even simple code can be difficult. I believe this is beneficial, as it compels programmers to understand all concepts before coding. However, it also suggests that this language may not be suitable for beginners.