From 27f2d2302d069570960ca73c59d603032d557b80 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Sun, 22 Oct 2023 15:57:37 +0200 Subject: [PATCH] book: Add section about tokio --- book/listings/main_event_loop/8/main.rs | 2 +- book/listings/main_event_loop/9/main.rs | 2 +- book/src/main_event_loop.md | 71 +++++++++++++++++++++---- 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/book/listings/main_event_loop/8/main.rs b/book/listings/main_event_loop/8/main.rs index eb00996eb0ee..1a42beb78c4c 100644 --- a/book/listings/main_event_loop/8/main.rs +++ b/book/listings/main_event_loop/8/main.rs @@ -43,7 +43,7 @@ fn build_ui(app: &Application) { main_context.spawn_local(clone!(@weak button => async move { while let Ok(response) = receiver.recv().await { if let Ok(response) = response { - println!("Status {}", response.status()); + println!("Status: {}", response.status()); } else { println!("Could not make a `GET` request."); } diff --git a/book/listings/main_event_loop/9/main.rs b/book/listings/main_event_loop/9/main.rs index 79f28cf055b6..291f48aeffa2 100644 --- a/book/listings/main_event_loop/9/main.rs +++ b/book/listings/main_event_loop/9/main.rs @@ -47,7 +47,7 @@ fn build_ui(app: &Application) { main_context.spawn_local(clone!(@weak button => async move { while let Ok(response) = receiver.recv().await { if let Ok(response) = response { - println!("Status {}", response.status()); + println!("Status: {}", response.status()); } else { println!("Could not make a `GET` request."); } diff --git a/book/src/main_event_loop.md b/book/src/main_event_loop.md index 00f466b03c89..e8a108f29b33 100644 --- a/book/src/main_event_loop.md +++ b/book/src/main_event_loop.md @@ -65,7 +65,7 @@ However, we don't want to block the main loop while waiting for a message to rec That is the whole point of the exercise after all! We solve that problem by waiting for messages to receive in an [`async`](https://rust-lang.github.io/async-book/) block. -We spawn that `async` block on the glib main loop with [`spawn_local`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.MainContext.html#method.spawn_local) (from other threads than the main thread [`spawn`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.MainContext.html#method.spawn) has to be used). +We spawn that `async` block on the `glib` main loop with [`spawn_local`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.MainContext.html#method.spawn_local) (from other threads than the main thread [`spawn`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.MainContext.html#method.spawn) has to be used). Filename: listings/main_event_loop/3/main.rs @@ -176,9 +176,9 @@ help: within `gtk4::Button`, the trait `Sync` is not implemented for `NonNullDialog requesting user information. +## Tokio + +[`tokio`](https://docs.rs/tokio/latest/tokio/) is Rust's most popular asynchronous platform. +Therefore, many high-quality crates are part of its ecosystem. +The web client [`reqwest`](https://docs.rs/reqwest/latest/reqwest/) belongs to this group. +Let's add it by executing the following command ``` cargo add reqwest@0.11 --features rustls-tls --no-default-features ``` +As soon as the button is pressed, we want to send a `GET` request to [www.gtk-rs.org](https://www.gtk-rs.org). +The response should then be sent to the main thread via a channel. + Filename: listings/main_event_loop/8/main.rs ```rust {{#rustdoc_include ../listings/main_event_loop/8/main.rs:callback}} ``` +This compiles fine and even seems to run. +However, nothing happens when we press the button. +Inspecting the console gives the following error message: + +``` +thread 'main' panicked at +'there is no reactor running, must be called from the context of a Tokio 1.x runtime' +``` + +At the time of writing, `reqwest` doesn't document this requirement. +Unfortunately, that is also the case for other libraries depending on `tokio`. +Let's bite the bullet and add `tokio`: + ``` cargo add tokio@1 --features rt-multi-thread ``` +Since we already run the `glib` main loop on our main thread, we don't want to run the `tokio` runtime there. +Let's bind it to a static variable and initialize it lazily. Filename: listings/main_event_loop/9/main.rs @@ -241,19 +265,44 @@ Filename: listings/main_event_loop/9/main.rs ```rust {{#rustdoc_include ../listings/main_event_loop/9/main.rs:callback}} ``` +If we now press the button, we should find the following message in our console: + +``` +Status: 200 OK +``` + +We will not need `tokio` or `reqwest` in the following chapters, so let's remove it again by executing: + +``` +cargo remove reqwest tokio +``` + +How to find out whether you can spawn an `async` task on the `glib` main loop? +You should be able to do that when the called functions come from libraries that either: +- come from the `glib` ecosystem, +- depend on `async-std`/`smol`, or +- have cargo features that let them depend on `async-std`/`smol` instead of `tokio`. + + +## Conclusion -So when should you spawn an `async` block and when should you spawn a thread? +You don't want to block the main thread long enough that it is noticable by the user. +But when should you spawn an `async` task, and when should you spawn a task in a separate thread? +Let's go again through the different scenarios. -# TODO +If you spend your time calculating rather than waiting for a web response, your task is [CPU-bound](https://en.wikipedia.org/wiki/CPU-bound). +That means you have to run your task in a separate thread and send results back via a channel. -- computation bound -> threads + channels -- IO: - - if you have glib/smol/async-std functions -> spawn on main loop (+ maybe channels) - - if tokio lib is better -> spawn on tokio + channels - - if those are not good options -> threads + channels +If your task is [IO bound](https://en.wikipedia.org/wiki/I/O_bound), it depends on the crates at your disposal +Functions from crates using `glib`/`smol`/`async-std` can be spawned on the main loop. +This typically leads to straightforward code, which can often avoid synchronization via channels. +If the best crate for the job relies on `tokio`, you will have to spawn it with the tokio runtime and send the results via channels.