Open cy6erninja opened 1 year ago
Futures are single eventual values produced by asynchronous computations. Some programming languages (e.g. JavaScript) call this concept “promise”. Streams represent a series of values produced asynchronously. Sinks provide support for asynchronous writing of data. Executors are responsible for running asynchronous tasks.
There are two primary executors that are available in Futures crate: ThreadPool and LocalPool. The former is capable of handling multiple tasks, spreading them across multiple threads in the pool for concurrent execution. This pool can have tasks implementing Send trait which allows them to be send between different threads. The latter is LocalPool and it creates single-threaded environment. Tasks can be without Send implementation. Since there is no need in synchronisation between threads, it reduces execution costs.
https://rust-lang.github.io/async-book/02_execution/02_future.html
Spawning tasks onto a LocalPool didn't work out. It requires complex trait bound to be implemented by the future we wanna spawn. With ThreadPool it is much easier, just pool.spawn_od(future) and that's it. An alternative and the simplest way to wait for async function aka future to be executed till the end is to use block_on function.
The difference between the above approaches is that block_on blocks current thread to wait for a future execution.
I have been learning about the Futures create and its inner workings. My intension was to reach basic principles it is grounded upon. No wonder these basic principles are almost completely consist of multiprocessor programming. I have learned how to spawn threads and wait for them to complete their work. A simple example of this would be:
use std::error::Error;
use std::thread;
fn main() -> Result<(), Box<dyn Error>> {
let data = 1;
let mut threads = vec![];
for _ in 0..2 {
let th = thread::spawn(move || {
println!("test");
println!("{:?}", data);
});
threads.push(th);
}
for th in threads {
let _ = th.join();
}
Ok(())
}
With array it didn't work as well as with a vector. When I was trying to call join function on a thread from an array, it said that the JoinHandle was moved, which probably means that I can not use array members since I'm executing join not on the reference to handle, but on the value itself and so it gets moved to another scope. With Vector, as I understand, I get to work with references to objects.
Reread rust book on a module system in rust. Found out how to use main.rs and lib.rs entry points at the same time. Both entry points are treated as a different creates(having the same name), thus I had to declare a crate as public in lib.rs in order to reach it from main.rs. Since lib.rs and main.rs are separate entry points to a package with the same name, I had to refer to a function inside the module in lib.rs in the following way:
use package_name::module_name::function_name;
See the doc
Started reading "The Art of Multiprocessor Programming" to dive in concepts better. Wrote first working version of the Philosophers problem.
use std::thread;
use std::time::Duration;
use rand::{thread_rng, Rng};
use std::vec::Vec;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct Chopstick {}
struct Philosopher {
name: String,
hand: Vec<Chopstick>
}
trait Thinker {
fn think(&self);
}
trait FoodConsumer {
fn eat(&mut self, chopstick_box: &Arc<Mutex<Vec<Chopstick>>>);
}
impl Thinker for Philosopher {
fn think(&self) {
let random_duration: u32 = thread_rng().gen();
println!("{} is gonna think for {} nano seconds...", &self.name, random_duration);
thread::sleep(Duration::from_nanos(random_duration.into()));
println!("{} is hungry...", &self.name);
}
}
impl FoodConsumer for Philosopher {
fn eat(&mut self, chopstick_box: &Arc<Mutex<Vec<Chopstick>>>) {
println!("{} tries taking 2 chopsticks...", &self.name);
if let Ok(mut holding_box) = chopstick_box.try_lock() {
if holding_box.len() < 2 {
println!("{} can't find enough chopsticks", &self.name);
return;
}
let _ = &self.hand.push(holding_box.pop().unwrap());
let _ = &self.hand.push(holding_box.pop().unwrap());
println!("{} took 2 chopsticks...", &self.name);
} else {
println!("{} could not take chopsticks, somebody is on it...", &self.name);
return;
}
let random_duration: u32 = thread_rng().gen();
println!("{} is gonna eat for {} nano seconds...", &self.name, random_duration);
thread::sleep(Duration::from_nanos(random_duration.into()));
println!("{} is finally full, returning chopsticks...", &self.name);
loop {
if self.hand.is_empty() {
break;
}
if let Ok(mut holding_box) = chopstick_box.try_lock() {
println!("{} is trying to return a chopstick...", &self.name);
holding_box.push(self.hand.pop().unwrap());
println!("{} returned 1 chopstick...", &self.name);
}
}
}
}
pub fn ex1()
{
const NUM: usize = 3;
let mut philosophers = vec![];
let chopsticks: Vec<Chopstick> = vec![Chopstick{}; 5];
println!("{:?}", chopsticks.len());
let shared_chopsticks = Arc::new(Mutex::new(chopsticks));
let names: [&str; NUM] = [
"Ben Franklin",
"Cicero",
"Socrat",
// "Marcus Aurelius",
// "Plato"
];
for i in 0..NUM {
let mut ph = Philosopher {name: String::from(names[i]), hand: vec![]};
let chopsticks_on_table = shared_chopsticks.clone();
philosophers.push(thread::spawn(move || {
loop {
ph.think();
ph.eat(&chopsticks_on_table);
}
}));
}
for p in philosophers {
p.join().expect("The thread has panicked...");
}
}
Experimenting a bit with smart pointers. It was a surprise to me that Rc smart pointer can be used as an alternative to a simple rust reference thanks to a Deref thread.
use std::rc::Rc;
pub fn learn_executor()
{
let str = "Some string";
let rc = Rc::new(str);
let rc1 = Rc::new(1);
test_rc(&rc); // works fine because we use a string when creating Rc
test_rc(&rc1); // does not work because the underlying object is not string
}
fn test_rc(somestr: &str) {
println!("I can print [{}]", somestr);
}
Similar stuff can be applied to other types. Just checked if it is the case with usize
use std::rc::Rc;
pub fn learn_executor()
{
let str = "Some string";
let rc = Rc::new(str);
let rc1 = Rc::new(1);
test_rc(&rc1);
}
fn test_rc(somestr: &usize) {
println!("I can print [{}]", somestr);
}
Decided to come back to actually implementing p2p tic tac toe 🥇 I thought, that libp2p crate works only with tokio to achieve async nature, but it turned out that there is also async_std available. As I don't wanna mess with tokio for now, I will investigate async_std
Also, unexpectedly, there is also std::future ин std . See async-rs docs
While trying to make libp2p::development_transport function available, I bumped into a similar issue on StackOverflow, which mentioned conditional compilation topic.
The idea of a conditional compilation is to better control the circumstances, under which some functionality becomes available. Since there are several dimensions for which compilation rules may be configured, it make sense to have a more subtle way to configure each unit of a library.
For example, in my case, it makes sense to enable "tcp" features along with development_transport, since the later depends on the former. But as it is an arbitrary choice whether to enable "tcp" or not, it is reasonable to enable the development_transport function only if all the conditions are met. Cargo.toml controls external dependencies whereas cfg! macro is capable of controlling internal ones.
Took a look at std::collections because I met VecDeque in a source code. It introduced me to 2 new concepts:
Encounter an error, saying that I'm importing libp2p::swarm::NetworkBehaviour trait, but don't have a derive macro for it. That took me awhile to find out that there is a separate feature that enables derive functionality for NetworkBehaviour. Also I got acquainted with FeatureFlags link on crate page and now I know where to find them.
The idea here is to start first with futures as an async programming approach and than move to tokio in the end. Regarding the transport and behaviour, I will start with dummy implementations from libp2p and than learn how to construct more complex things.