main.rs (5563B)
1 //! An exercise to create a simple actor 2 //! 3 //! What is an actor? 4 //! 5 //! The basic idea behind an actor is to spawn a self-contained task that performs 6 //! some job independently of other parts of the program. 7 //! 8 //! Typically these actors communicate with the rest of the program through the 9 //! use of message passing channels. 10 //! 11 //! Since each actor runs independently, programs designed using them are 12 //! naturally parallel. 13 //! 14 //! Things to pay attention to: 15 //! 1. Where to put the tokio::spawn call. 16 //! 2. Struct with run method vs bare function. 17 //! 3. andles to the actor. 18 //! 4. Backpressure and bounded channels. 19 //! 5. Graceful shutdown. 20 21 use tokio::sync::{mpsc, oneshot}; 22 23 /// An actor is split into two parts: the task and the handle. 24 /// 25 /// The task is the independently spawned Tokio task that actually performs 26 /// the duties of the actor, and the handle is a struct that allows you to 27 /// communicate with the task. 28 struct MyActor { 29 receiver: mpsc::Receiver<ActorMessage>, 30 next_id: u32, 31 } 32 33 /// The ActorMessage enum defines the kind of messages we can send to the actor. 34 /// 35 /// By using an enum, we can have many different message types, and each message 36 /// type can have its own set of arguments. We return a value to the sender by 37 /// using an oneshot channel, which is a message passing channel that allows 38 /// sending exactly one message. 39 enum ActorMessage { 40 GetUniqueId { respond_to: oneshot::Sender<u32> }, 41 } 42 43 impl MyActor { 44 fn new(receiver: mpsc::Receiver<ActorMessage>) -> Self { 45 MyActor { 46 receiver, 47 next_id: 0, 48 } 49 } 50 51 fn handle_message(&mut self, msg: ActorMessage) { 52 // We match on the enum inside a handle_message method on the actor struct, 53 // but that isn't the only way to structure this. One could also match on 54 // the enum in the run_my_actor function. Each branch in this match could 55 // then call various methods such as get_unique_id on the actor object. 56 match msg { 57 ActorMessage::GetUniqueId { respond_to } => { 58 self.next_id += 1; 59 60 // The `let _ =` ignores any errors when sending. 61 // 62 // This can happen if the `select!` macro is used 63 // to cancel waiting for the response. 64 let _ = respond_to.send(self.next_id); 65 } 66 } 67 } 68 } 69 70 async fn run_my_actor(mut actor: MyActor) { 71 // We can detect when the actor should shut down by looking at failures to 72 // receive messages. In our example, this happens in the following while loop: 73 while let Some(msg) = actor.receiver.recv().await { 74 actor.handle_message(msg); 75 } 76 } 77 78 /// Now that we have the actor itself, we also need a handle to the actor. 79 /// 80 /// A handle is an object that other pieces of code can use to talk to the actor, 81 /// and is also what keeps the actor alive. 82 /// 83 /// Derive Clone: Since the channel allows multiple producers, we can freely 84 /// clone our handle to the actor, allowing us to talk to it from multiple places. 85 #[derive(Clone)] 86 pub struct MyActorHandle { 87 sender: mpsc::Sender<ActorMessage>, 88 } 89 90 impl MyActorHandle { 91 pub fn new() -> Self { 92 let (sender, receiver) = mpsc::channel(8); 93 let actor = MyActor::new(receiver); 94 tokio::spawn(run_my_actor(actor)); 95 96 Self { sender } 97 } 98 99 pub async fn get_unique_id(&self) -> u32 { 100 let (send, recv) = oneshot::channel(); 101 let msg = ActorMessage::GetUniqueId { respond_to: send }; 102 103 // Ignore send errors. If this send fails, so does the 104 // recv.await below. There's no reason to check for the 105 // same failure twice. 106 let _ = self.sender.send(msg).await; 107 recv.await.expect("Actor task has been killed") 108 } 109 } 110 111 /// When you call MyActorHandle::new(), it already calls tokio::spawn(run_my_actor(actor)) 112 /// internally. So by the time new() returns, the actor task is live and waiting 113 /// for messages on its mpsc::Receiver. 114 /// 115 /// You never call tokio::spawn in main — it's encapsulated inside MyActorHandle::new(), 116 /// which is the idiomatic placement for this pattern. This keeps the spawning 117 /// logic close to the actor itself. 118 /// 119 /// Graceful shutdown is automatic — when actor_handle (and all its clones) are 120 /// dropped, the mpsc::Sender is dropped, causing actor.receiver.recv().await to 121 /// return None, breaking the while let loop and ending the task. 122 /// 123 /// Cloning the handle is safe — MyActorHandle derives Clone, so multiple parts 124 /// of your program can send messages to the same actor concurrently without any 125 /// extra synchronization, since the actor processes them one at a time. 126 /// 127 /// Backpressure is built in — the channel is bounded (mpsc::channel(8)), so if 128 /// the actor can't keep up, senders will .await until there's room, naturally 129 /// throttling the workload. 130 #[tokio::main] 131 async fn main() { 132 // 1. Creating the handle also spawns the actor task automatically (inside MyActorHandle::new) 133 let actor_handle = MyActorHandle::new(); 134 135 // 2. Send a message to the actor and await the response 136 let id1 = actor_handle.get_unique_id().await; 137 println!("Got id: {}", id1); // prints 1 138 139 let id2 = actor_handle.get_unique_id().await; 140 println!("Got id: {}", id2); // prints 2 141 142 // 3. Clone the handle to show multiple owners can talk to the same actor 143 let handle2 = actor_handle.clone(); 144 let id3 = handle2.get_unique_id().await; 145 println!("Got id from cloned handle: {}", id3); // prints 3 146 }