cy6erninja / p2p-tic-tac-toe-rs

Noob implementation of cli p2p tictactoe in Rust
0 stars 0 forks source link

Create a dummy implementation of p2p communication using futures, dummy development transport and ping network behaviour #2

Open cy6erninja opened 1 year ago

cy6erninja commented 1 year ago

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.

cy6erninja commented 1 year ago

Four main primitives in Futures crate

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.

cy6erninja commented 1 year ago

Summary of the game in a mindmap

image
cy6erninja commented 1 year ago

Learning Futures create

Executors

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.

Future trait

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.

cy6erninja commented 1 year ago

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.

cy6erninja commented 1 year ago

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

cy6erninja commented 1 year ago

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...");
    }
}
cy6erninja commented 1 year ago

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);
}
cy6erninja commented 1 year ago

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

cy6erninja commented 1 year ago

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.

cy6erninja commented 1 year ago

Took a look at std::collections because I met VecDeque in a source code. It introduced me to 2 new concepts:

cy6erninja commented 1 year ago

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.