From 3641400e17e5a7553f708012b95134e8971b906e Mon Sep 17 00:00:00 2001 From: Hofer-Julian <30049909+Hofer-Julian@users.noreply.github.com> Date: Sun, 22 Oct 2023 12:55:56 +0200 Subject: [PATCH] book: Move to async-channel (#1521) async-channel covers more use cases. This also fits with my observation that it is popular within gtk-rs apps --- book/listings/Cargo.lock | 36 +++++++++++++++++ book/listings/Cargo.toml | 1 + book/listings/main_event_loop/3/main.rs | 29 +++++++------- book/listings/main_event_loop/4/main.rs | 26 ++++++------- book/listings/main_event_loop/5/main.rs | 1 - book/src/main_event_loop.md | 51 +++++++++++++++---------- 6 files changed, 93 insertions(+), 51 deletions(-) diff --git a/book/listings/Cargo.lock b/book/listings/Cargo.lock index 2b9f09902d0c..748cf4ef0a04 100644 --- a/book/listings/Cargo.lock +++ b/book/listings/Cargo.lock @@ -8,6 +8,17 @@ version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -67,6 +78,24 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "concurrent-queue" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + [[package]] name = "dirs" version = "5.0.1" @@ -94,6 +123,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "field-offset" version = "0.3.6" @@ -426,6 +461,7 @@ name = "gtk4-rs-book-listings" version = "0.1.0" dependencies = [ "anyhow", + "async-channel", "dirs", "glib-build-tools", "gtk4", diff --git a/book/listings/Cargo.toml b/book/listings/Cargo.toml index 593e0ed086bb..93b9535d4ef2 100644 --- a/book/listings/Cargo.toml +++ b/book/listings/Cargo.toml @@ -14,6 +14,7 @@ anyhow = "1.0" xshell = "0.2" dirs = "5.0" walkdir = "2.3" +async-channel = "1.9.0" [build-dependencies] glib-build-tools = "0.18" diff --git a/book/listings/main_event_loop/3/main.rs b/book/listings/main_event_loop/3/main.rs index 8832fdc335f2..8a9944e285f7 100644 --- a/book/listings/main_event_loop/3/main.rs +++ b/book/listings/main_event_loop/3/main.rs @@ -1,7 +1,7 @@ use std::thread; use std::time::Duration; -use glib::{clone, MainContext, Priority}; +use glib::{clone, MainContext}; use gtk::prelude::*; use gtk::{gio, glib, Application, ApplicationWindow, Button}; @@ -29,31 +29,32 @@ fn build_ui(app: &Application) { .build(); // ANCHOR: callback - let (sender, receiver) = MainContext::channel(Priority::default()); + let (sender, receiver) = async_channel::unbounded(); // Connect to "clicked" signal of `button` button.connect_clicked(move |_| { let sender = sender.clone(); // The long running operation runs now in a separate thread gio::spawn_blocking(move || { // Deactivate the button until the operation is done - sender.send(false).expect("Could not send through channel"); + sender + .send_blocking(false) + .expect("The channel needs to be open."); let ten_seconds = Duration::from_secs(10); thread::sleep(ten_seconds); // Activate the button again - sender.send(true).expect("Could not send through channel"); + sender + .send_blocking(true) + .expect("The channel needs to be open."); }); }); - // The main loop executes the closure as soon as it receives the message - receiver.attach( - None, - clone!(@weak button => @default-return glib::ControlFlow::Break, - move |enable_button| { - button.set_sensitive(enable_button); - glib::ControlFlow::Continue - } - ), - ); + let main_context = MainContext::default(); + // The main loop executes the asynchronous block + main_context.spawn_local(clone!(@weak button => async move { + while let Ok(enable_button) = receiver.recv().await { + button.set_sensitive(enable_button); + } + })); // ANCHOR_END: callback // Create a window diff --git a/book/listings/main_event_loop/4/main.rs b/book/listings/main_event_loop/4/main.rs index 8530571bca2c..6c8fc44e3b1d 100644 --- a/book/listings/main_event_loop/4/main.rs +++ b/book/listings/main_event_loop/4/main.rs @@ -1,4 +1,4 @@ -use glib::{clone, MainContext, Priority}; +use glib::{clone, MainContext}; use gtk::prelude::*; use gtk::{glib, Application, ApplicationWindow, Button}; @@ -26,30 +26,26 @@ fn build_ui(app: &Application) { .build(); // ANCHOR: callback - let (sender, receiver) = MainContext::channel(Priority::default()); + let (sender, receiver) = async_channel::unbounded(); // Connect to "clicked" signal of `button` button.connect_clicked(move |_| { let main_context = MainContext::default(); - // The main loop executes the asynchronous block main_context.spawn_local(clone!(@strong sender => async move { // Deactivate the button until the operation is done - sender.send(false).expect("Could not send through channel"); + sender.send(false).await.expect("The channel needs to be open."); glib::timeout_future_seconds(5).await; // Activate the button again - sender.send(true).expect("Could not send through channel"); + sender.send(true).await.expect("The channel needs to be open."); })); }); - // The main loop executes the closure as soon as it receives the message - receiver.attach( - None, - clone!(@weak button => @default-return glib::ControlFlow::Break, - move |enable_button| { - button.set_sensitive(enable_button); - glib::ControlFlow::Continue - } - ), - ); + let main_context = MainContext::default(); + // The main loop executes the asynchronous block + main_context.spawn_local(clone!(@weak button => async move { + while let Ok(enable_button) = receiver.recv().await { + button.set_sensitive(enable_button); + } + })); // ANCHOR_END: callback // Create a window diff --git a/book/listings/main_event_loop/5/main.rs b/book/listings/main_event_loop/5/main.rs index 2081e595c5c6..747588381002 100644 --- a/book/listings/main_event_loop/5/main.rs +++ b/book/listings/main_event_loop/5/main.rs @@ -29,7 +29,6 @@ fn build_ui(app: &Application) { // Connect to "clicked" signal of `button` button.connect_clicked(move |button| { let main_context = MainContext::default(); - // The main loop executes the asynchronous block main_context.spawn_local(clone!(@weak button => async move { // Deactivate the button until the operation is done button.set_sensitive(false); diff --git a/book/src/main_event_loop.md b/book/src/main_event_loop.md index 5add22fca97b..3553d03af161 100644 --- a/book/src/main_event_loop.md +++ b/book/src/main_event_loop.md @@ -11,10 +11,9 @@ It does all of that within the same thread. Quickly iterating between all tasks gives the illusion of parallelism. That is why you can move the window at the same time as a progress bar is growing. - However, you surely saw GUIs that became unresponsive, at least for a few seconds. That happens when a single task takes too long. -Let's look at one example. +The following example uses [`std::thread::sleep`](https://doc.rust-lang.org/std/thread/fn.sleep.html) to represent a long-running task. Filename: listings/main_event_loop/1/main.rs @@ -29,7 +28,7 @@ but it is not unusual wanting to run a slightly longer operation in one go. ## How to Avoid Blocking the Main Loop -In order to avoid blocking the main loop we can spawn a new thread with [`gio::spawn_blocking`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/gio/fn.spawn_blocking.html) and let the operation run there. +In order to avoid blocking the main loop we can spawn a new task with [`gio::spawn_blocking`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/gio/fn.spawn_blocking.html) and let the operation run there. Filename: listings/main_event_loop/2/main.rs @@ -45,16 +44,28 @@ Filename: listings/main_event_loop/3/main.rs @@ -62,6 +73,10 @@ Filename: listings/main_event_loop/4/main.rs @@ -86,7 +95,7 @@ Filename: listings/main_event_loop/5/main.rs @@ -94,7 +103,7 @@ Filename: