Skip to content

Commit

Permalink
book: Move to async-channel
Browse files Browse the repository at this point in the history
async-channel covers more use cases.
This also fits with my observation that it is popular within gtk-rs apps
  • Loading branch information
Hofer-Julian committed Oct 22, 2023
1 parent d0094d7 commit 35bd0e3
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 48 deletions.
36 changes: 36 additions & 0 deletions book/listings/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions book/listings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
29 changes: 15 additions & 14 deletions book/listings/main_event_loop/3/main.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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
Expand Down
26 changes: 11 additions & 15 deletions book/listings/main_event_loop/4/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use glib::{clone, MainContext, Priority};
use glib::{clone, MainContext};
use gtk::prelude::*;
use gtk::{glib, Application, ApplicationWindow, Button};

Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion book/listings/main_event_loop/5/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
45 changes: 27 additions & 18 deletions book/src/main_event_loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <a class=file-link href="https://github.com/gtk-rs/gtk4-rs/blob/master/book/listings/main_event_loop/1/main.rs">listings/main_event_loop/1/main.rs</a>

Expand All @@ -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: <a class=file-link href="https://github.com/gtk-rs/gtk4-rs/blob/master/book/listings/main_event_loop/2/main.rs">listings/main_event_loop/2/main.rs</a>

Expand All @@ -45,39 +44,49 @@ Filename: <a class=file-link href="https://github.com/gtk-rs/gtk4-rs/blob/master
</div>


> If you come from another language than Rust, you might be uncomfortable with the thought of spawning new threads before even looking at other options.
> If you come from another language than Rust, you might be uncomfortable with the thought of running tasks in separate threads before even looking at other options.
> Luckily, Rust's safety guarantees allow you to stop worrying about the nasty bugs that concurrency tends to bring.

Typically, we want to keep track of the work in the task.
In our case, we don't want the user to spawn additional tasks while an existing one is still running.
In order to achieve that we can create a channel with the crate [`async-channel`](https://docs.rs/async-channel/latest/async_channel/index.html).
Let's add it by executing the following in the terminal:

```
cargo add async-channel
```

Normally we want to keep track of the work in the thread.
In our case, we don't want the user to spawn additional threads while an existing one is still running.
In order to achieve that we can create a channel.
The main loop allows us to send a message from multiple places to a single receiver at the main thread.
We want to send a `bool` to inform, whether we want the button to react to clicks or not.
Since we send in a separate thread, we can use [`send_blocking`](https://docs.rs/async-channel/latest/async_channel/struct.Sender.html#method.send_blocking).
But what about receiving?
Every time we get a message we want to set the sensitivity of the button according to the `bool` we've received.
However, we don't want to block the main loop while waiting for a message to receive.
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).

Filename: <a class=file-link href="https://github.com/gtk-rs/gtk4-rs/blob/master/book/listings/main_event_loop/3/main.rs">listings/main_event_loop/3/main.rs</a>

```rust
{{#rustdoc_include ../listings/main_event_loop/3/main.rs:callback}}
```

As you can see, spawning a task still doesn't freeze our user interface.
Now, we also can't spawn multiple tasks at the same time since the button becomes insensitive after the first task has been spawned.
After the task is finished, the button becomes sensitive again.

<div style="text-align:center">
<video autoplay muted loop>
<source src="vid/main_event_loop_3.webm" type="video/webm">
<p>The button now stops being responsive for 10 seconds after being pressed</p>
</video>
</div>


> Per default, [`glib::clone!`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/macro.clone.html) returns `()` when upgrading of a weak reference fails.
> [`glib::Receiver::attach`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.Receiver.html#method.attach) expects a closure with a return value of type [`glib::ControlFlow`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/enum.ControlFlow.html).
> This is why we specify `@default-return` as `glib::ControlFlow::Break` to clarify that the closure not be called anymore as soon as the upgrade of a weak reference fails.

Spawning threads is not the only way to run operations asynchronously.
You can also let the main loop take care of running [`async`](https://rust-lang.github.io/async-book/) functions.
If you do that from the main thread use [`spawn_local`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.MainContext.html#method.spawn_local), from other threads [`spawn`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.MainContext.html#method.spawn) has to be used.
What if the task is asynchronous by nature?
Let's use [`glib::timeout_future_seconds`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/fn.timeout_future_seconds.html) as represenation for our task instead of `std::thread::slepp`.

Check warning on line 88 in book/src/main_event_loop.md

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"represenation" should be "representation".
It returns a [`std::future::Future`](https://doc.rust-lang.org/std/future/trait.Future.html), which means we can `await` on it within an `async` context.
The converted code looks and behaves very similar to the multi-threaded code.

Filename: <a class=file-link href="https://github.com/gtk-rs/gtk4-rs/blob/master/book/listings/main_event_loop/4/main.rs">listings/main_event_loop/4/main.rs</a>
Expand All @@ -86,7 +95,7 @@ Filename: <a class=file-link href="https://github.com/gtk-rs/gtk4-rs/blob/master
{{#rustdoc_include ../listings/main_event_loop/4/main.rs:callback}}
```

Since we are single-threaded again, we could even get rid of the channels while achieving the same result.
Since we are single-threaded again, we can even get rid of the channel while achieving the same result.

Filename: <a class=file-link href="https://github.com/gtk-rs/gtk4-rs/blob/master/book/listings/main_event_loop/5/main.rs">listings/main_event_loop/5/main.rs</a>

Expand Down

0 comments on commit 35bd0e3

Please sign in to comment.