Rust lang series episode #30 — channels (#rust-series)
Hello everyone, 30th Rust lang series episode is here. In previous episode, we were working with multiple threads. Today we will extend it with knowledge of channels.
Channels
Channel in Rust is facility to send and receive data across threads. Chanel sends generic signal and we can also send any data over the channel.
Creating channel
let (tx,rx) = mpsc::channel();
tx is channel Transmitter/Sender
rx is channel Receiver
With channels we can achieve two types of communication
- asynchronous buffered channel from Sender to Receiver
- synchronous buffered channel from SyncSender to Receiver
Sending data
tx.send(10).unwrap();
Sending number via channels
fn main() {
use std::thread;
use std::sync::mpsc::channel;
let (tx, rx) = channel();
thread::spawn(move || {
tx.send(10).unwrap();
});
println!("Data from channel received: {}",rx.recv().unwrap());
}
Breaking down
channel() function creates transaction channel providing tuple with transmitter and receiver.
send() method sends value over channel to a receiver
recv() waits for channel to receive sent data
Again consider finer error handling for send() and recv().
Multiple threads example
Now we can boost this example to create multiple threads and receive their data via channels. This will compute third power on each thread for numbers from 0 to 9.
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
for i in 0..10 {
let tx = tx.clone();
thread::spawn(move || {
let answer = i * i * i;
tx.send(answer).unwrap();
});
}
for _ in 0..10 {
println!("{}", rx.recv().unwrap());
}
}
Output
0
8
27
...
Note that result order can vary.
Breaking down
There is actually nothing unknown here except we use clone() to create copy of transmitter for each thread. And we call recv() in cycle to capture channel data one by one.
Heating up your CPU
Well, so far we've used threads for operations that can be easily done on single thread. But we can see true performance upgrade when we try to compute big numbers for example. Check this modified example that will compute factorial for number in range from 10,000 to 10,020. This will finally work your CPU hard. We will need crate for big numbers so add this to your Cargo.toml
num = "*"
And code can look like this to perform big number factorization.
extern crate num;
use num::{BigUint, One, FromPrimitive};
use std::mem::replace;
use std::thread;
use std::sync::mpsc;
fn factorial(n: usize) -> BigUint {
let mut f: BigUint = One::one();
for i in 1..(n+1) {
let bu: BigUint = FromPrimitive::from_usize(i).unwrap();
f = f * bu;
}
f
}
fn main() {
let (tx, rx) = mpsc::channel();
for i in 10000..10020 {
let tx = tx.clone();
thread::spawn(move || {
let answer = factorial(i);
let res = (i, answer);
tx.send(res).unwrap();
});
}
for n in 0..20 {
let (n,res) = rx.recv().unwrap();
println!("{}!={}", n, res);
}
println!("Computation finished!");
}
Output
10003!=28479677...
10000!=28462...
Computation finished!
When you run this you can see all cores (unless you have more than 20 cores :)) of your CPU fully utilized! Code utilizes BigUint from num crate to compute numbers much bigger than the range of 64-bit integers. Note that the work with num crate is little bit less comfortable than with built-in primitive numeric types but on the other hand we can work with really big numbers which is exactly what we need here.
Postfix
That's all for now, thank you for your appreciations, feel free to comment and point out possible mistakes (first 24 hours works the best but any time is fine). May Jesus bless your programming skills, use them wisely and see you next time.
Meanwhile you can also check the official documentation for additional related information: