103-borrowing-shared — Shared Borrowing
Tutorial
The Problem
Safe concurrent read access is the foundation of parallel computing. In C, multiple threads reading the same data simultaneously is fine as long as no thread writes, but the language does not enforce this rule — a race condition occurs silently. Rust's borrow checker enforces the reader-writer invariant at compile time: any number of shared references (&T) can coexist, but no mutable reference (&mut T) can coexist with any other reference.
This is Rust's implementation of the "readers-writers" lock at zero runtime cost — the type system acts as the lock.
🎯 Learning Outcomes
&T references can coexist&T to functions without transferring ownershipCode Example
#![allow(clippy::all)]
// 103: Shared Borrowing — &T
// Multiple readers, no writers
fn sum(data: &[i32]) -> i32 {
data.iter().sum()
}
fn count(data: &[i32]) -> usize {
data.len()
}
fn average(data: &[i32]) -> f64 {
// Multiple shared borrows simultaneously — perfectly safe
let s = sum(data); // &data borrow 1
let c = count(data); // &data borrow 2 — fine!
s as f64 / c as f64
}
fn first_and_last(data: &[i32]) -> Option<(i32, i32)> {
if data.is_empty() {
None
} else {
Some((data[0], data[data.len() - 1]))
}
}
// Multiple shared references can coexist
fn demonstrate_multiple_borrows() {
let data = vec![1, 2, 3, 4, 5];
let r1 = &data;
let r2 = &data;
let r3 = &data;
// All three references valid simultaneously
println!("r1={:?}, r2={:?}, r3={:?}", r1[0], r2[1], r3[2]);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_average() {
assert!((average(&[1, 2, 3, 4, 5]) - 3.0).abs() < 0.001);
}
#[test]
fn test_first_and_last() {
assert_eq!(first_and_last(&[10, 20, 30]), Some((10, 30)));
assert_eq!(first_and_last(&[]), None);
}
#[test]
fn test_multiple_borrows() {
let v = vec![1, 2, 3];
let r1 = &v;
let r2 = &v;
assert_eq!(r1.len(), r2.len());
}
}Key Differences
&T reference means the data cannot change — no locking needed for read-only parallel access; OCaml requires explicit synchronisation for safe concurrent mutation.OCaml Approach
OCaml has no borrow checking. Any binding can read any value at any time because the GC manages all values:
let data = [1; 2; 3; 4; 5]
let s = List.fold_left (+) 0 data (* data still accessible *)
let n = List.length data (* data still accessible *)
let avg = float_of_int s /. float_of_int n
Shared reading is always safe in OCaml because the GC ensures the data lives as long as any reference exists. There is no way to express or enforce mutation exclusivity at the type level (without external libraries like Base.Ref with locking).
Full Source
#![allow(clippy::all)]
// 103: Shared Borrowing — &T
// Multiple readers, no writers
fn sum(data: &[i32]) -> i32 {
data.iter().sum()
}
fn count(data: &[i32]) -> usize {
data.len()
}
fn average(data: &[i32]) -> f64 {
// Multiple shared borrows simultaneously — perfectly safe
let s = sum(data); // &data borrow 1
let c = count(data); // &data borrow 2 — fine!
s as f64 / c as f64
}
fn first_and_last(data: &[i32]) -> Option<(i32, i32)> {
if data.is_empty() {
None
} else {
Some((data[0], data[data.len() - 1]))
}
}
// Multiple shared references can coexist
fn demonstrate_multiple_borrows() {
let data = vec![1, 2, 3, 4, 5];
let r1 = &data;
let r2 = &data;
let r3 = &data;
// All three references valid simultaneously
println!("r1={:?}, r2={:?}, r3={:?}", r1[0], r2[1], r3[2]);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_average() {
assert!((average(&[1, 2, 3, 4, 5]) - 3.0).abs() < 0.001);
}
#[test]
fn test_first_and_last() {
assert_eq!(first_and_last(&[10, 20, 30]), Some((10, 30)));
assert_eq!(first_and_last(&[]), None);
}
#[test]
fn test_multiple_borrows() {
let v = vec![1, 2, 3];
let r1 = &v;
let r2 = &v;
assert_eq!(r1.len(), r2.len());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_average() {
assert!((average(&[1, 2, 3, 4, 5]) - 3.0).abs() < 0.001);
}
#[test]
fn test_first_and_last() {
assert_eq!(first_and_last(&[10, 20, 30]), Some((10, 30)));
assert_eq!(first_and_last(&[]), None);
}
#[test]
fn test_multiple_borrows() {
let v = vec![1, 2, 3];
let r1 = &v;
let r2 = &v;
assert_eq!(r1.len(), r2.len());
}
}
Deep Comparison
Core Insight
Shared borrows allow multiple simultaneous readers — the compiler guarantees no one writes during shared access
OCaml Approach
Rust Approach
Comparison Table
| Feature | OCaml | Rust |
|---|---|---|
| See | example.ml | example.rs |
Exercises
stats(data: &[f64]) -> (f64, f64, f64) function that returns (min, max, mean) using three separate passes over the borrowed slice.ReadOnlyView<'a, T> { data: &'a [T] } that exposes read-only operations and confirm you cannot store a &mut [T] in it.&data to two concurrent threads (using std::thread::scope) works without locks when neither thread mutates the data.