Skip to content

Commit 3641400

Browse files
authored
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
1 parent d0094d7 commit 3641400

File tree

6 files changed

+93
-51
lines changed

6 files changed

+93
-51
lines changed

book/listings/Cargo.lock

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

book/listings/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ anyhow = "1.0"
1414
xshell = "0.2"
1515
dirs = "5.0"
1616
walkdir = "2.3"
17+
async-channel = "1.9.0"
1718

1819
[build-dependencies]
1920
glib-build-tools = "0.18"

book/listings/main_event_loop/3/main.rs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::thread;
22
use std::time::Duration;
33

4-
use glib::{clone, MainContext, Priority};
4+
use glib::{clone, MainContext};
55
use gtk::prelude::*;
66
use gtk::{gio, glib, Application, ApplicationWindow, Button};
77

@@ -29,31 +29,32 @@ fn build_ui(app: &Application) {
2929
.build();
3030

3131
// ANCHOR: callback
32-
let (sender, receiver) = MainContext::channel(Priority::default());
32+
let (sender, receiver) = async_channel::unbounded();
3333
// Connect to "clicked" signal of `button`
3434
button.connect_clicked(move |_| {
3535
let sender = sender.clone();
3636
// The long running operation runs now in a separate thread
3737
gio::spawn_blocking(move || {
3838
// Deactivate the button until the operation is done
39-
sender.send(false).expect("Could not send through channel");
39+
sender
40+
.send_blocking(false)
41+
.expect("The channel needs to be open.");
4042
let ten_seconds = Duration::from_secs(10);
4143
thread::sleep(ten_seconds);
4244
// Activate the button again
43-
sender.send(true).expect("Could not send through channel");
45+
sender
46+
.send_blocking(true)
47+
.expect("The channel needs to be open.");
4448
});
4549
});
4650

47-
// The main loop executes the closure as soon as it receives the message
48-
receiver.attach(
49-
None,
50-
clone!(@weak button => @default-return glib::ControlFlow::Break,
51-
move |enable_button| {
52-
button.set_sensitive(enable_button);
53-
glib::ControlFlow::Continue
54-
}
55-
),
56-
);
51+
let main_context = MainContext::default();
52+
// The main loop executes the asynchronous block
53+
main_context.spawn_local(clone!(@weak button => async move {
54+
while let Ok(enable_button) = receiver.recv().await {
55+
button.set_sensitive(enable_button);
56+
}
57+
}));
5758
// ANCHOR_END: callback
5859

5960
// Create a window

book/listings/main_event_loop/4/main.rs

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use glib::{clone, MainContext, Priority};
1+
use glib::{clone, MainContext};
22
use gtk::prelude::*;
33
use gtk::{glib, Application, ApplicationWindow, Button};
44

@@ -26,30 +26,26 @@ fn build_ui(app: &Application) {
2626
.build();
2727

2828
// ANCHOR: callback
29-
let (sender, receiver) = MainContext::channel(Priority::default());
29+
let (sender, receiver) = async_channel::unbounded();
3030
// Connect to "clicked" signal of `button`
3131
button.connect_clicked(move |_| {
3232
let main_context = MainContext::default();
33-
// The main loop executes the asynchronous block
3433
main_context.spawn_local(clone!(@strong sender => async move {
3534
// Deactivate the button until the operation is done
36-
sender.send(false).expect("Could not send through channel");
35+
sender.send(false).await.expect("The channel needs to be open.");
3736
glib::timeout_future_seconds(5).await;
3837
// Activate the button again
39-
sender.send(true).expect("Could not send through channel");
38+
sender.send(true).await.expect("The channel needs to be open.");
4039
}));
4140
});
4241

43-
// The main loop executes the closure as soon as it receives the message
44-
receiver.attach(
45-
None,
46-
clone!(@weak button => @default-return glib::ControlFlow::Break,
47-
move |enable_button| {
48-
button.set_sensitive(enable_button);
49-
glib::ControlFlow::Continue
50-
}
51-
),
52-
);
42+
let main_context = MainContext::default();
43+
// The main loop executes the asynchronous block
44+
main_context.spawn_local(clone!(@weak button => async move {
45+
while let Ok(enable_button) = receiver.recv().await {
46+
button.set_sensitive(enable_button);
47+
}
48+
}));
5349
// ANCHOR_END: callback
5450

5551
// Create a window

book/listings/main_event_loop/5/main.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ fn build_ui(app: &Application) {
2929
// Connect to "clicked" signal of `button`
3030
button.connect_clicked(move |button| {
3131
let main_context = MainContext::default();
32-
// The main loop executes the asynchronous block
3332
main_context.spawn_local(clone!(@weak button => async move {
3433
// Deactivate the button until the operation is done
3534
button.set_sensitive(false);

book/src/main_event_loop.md

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ It does all of that within the same thread.
1111
Quickly iterating between all tasks gives the illusion of parallelism.
1212
That is why you can move the window at the same time as a progress bar is growing.
1313

14-
1514
However, you surely saw GUIs that became unresponsive, at least for a few seconds.
1615
That happens when a single task takes too long.
17-
Let's look at one example.
16+
The following example uses [`std::thread::sleep`](https://doc.rust-lang.org/std/thread/fn.sleep.html) to represent a long-running task.
1817

1918
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>
2019

@@ -29,7 +28,7 @@ but it is not unusual wanting to run a slightly longer operation in one go.
2928

3029
## How to Avoid Blocking the Main Loop
3130

32-
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.
31+
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.
3332

3433
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>
3534

@@ -45,56 +44,66 @@ Filename: <a class=file-link href="https://github.com/gtk-rs/gtk4-rs/blob/master
4544
</div>
4645

4746

48-
> 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.
47+
> 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.
4948
> Luckily, Rust's safety guarantees allow you to stop worrying about the nasty bugs that concurrency tends to bring.
5049
5150

51+
Typically, we want to keep track of the work in the task.
52+
In our case, we don't want the user to spawn additional tasks while an existing one is still running.
53+
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).
54+
Let's add it by executing the following in the terminal:
55+
56+
```
57+
cargo add async-channel
58+
```
5259

53-
Normally we want to keep track of the work in the thread.
54-
In our case, we don't want the user to spawn additional threads while an existing one is still running.
55-
In order to achieve that we can create a channel.
56-
The main loop allows us to send a message from multiple places to a single receiver at the main thread.
5760
We want to send a `bool` to inform, whether we want the button to react to clicks or not.
61+
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).
62+
But what about receiving?
63+
Every time we get a message we want to set the sensitivity of the button according to the `bool` we've received.
64+
However, we don't want to block the main loop while waiting for a message to receive.
65+
That is the whole point of the exercise after all!
66+
67+
We solve that problem by waiting for messages to receive in an [`async`](https://rust-lang.github.io/async-book/) block.
68+
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).
5869

5970
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>
6071

6172
```rust
6273
{{#rustdoc_include ../listings/main_event_loop/3/main.rs:callback}}
6374
```
6475

76+
As you can see, spawning a task still doesn't freeze our user interface.
77+
Now, we also can't spawn multiple tasks at the same time since the button becomes insensitive after the first task has been spawned.
78+
After the task is finished, the button becomes sensitive again.
79+
6580
<div style="text-align:center">
6681
<video autoplay muted loop>
6782
<source src="vid/main_event_loop_3.webm" type="video/webm">
6883
<p>The button now stops being responsive for 10 seconds after being pressed</p>
6984
</video>
7085
</div>
7186

72-
73-
> 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.
74-
> [`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).
75-
> 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.
76-
77-
78-
Spawning threads is not the only way to run operations asynchronously.
79-
You can also let the main loop take care of running [`async`](https://rust-lang.github.io/async-book/) functions.
80-
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.
81-
The converted code looks and behaves very similar to the multi-threaded code.
87+
What if the task is asynchronous by nature?
88+
Let's use [`glib::timeout_future_seconds`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/fn.timeout_future_seconds.html) as representation for our task instead of `std::thread::slepp`.
89+
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.
90+
The converted code looks and behaves very similar to the multithreaded code.
8291

8392
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>
8493

8594
```rust
8695
{{#rustdoc_include ../listings/main_event_loop/4/main.rs:callback}}
8796
```
8897

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

91100
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>
92101

93102
```rust
94103
{{#rustdoc_include ../listings/main_event_loop/5/main.rs:callback}}
95104
```
96105

97-
But why did we not do the same thing with our multi-threaded example?
106+
But why did we not do the same thing with our multithreaded example?
98107

99108
```rust ,no_run,compile_fail
100109
# use std::{thread, time::Duration};
@@ -167,6 +176,6 @@ help: within `gtk4::Button`, the trait `Sync` is not implemented for `NonNull<GO
167176

168177
After reference cycles we found the second disadvantage of GTK GObjects: They are not thread safe.
169178

170-
So when should you spawn an `async` block and when should you spawn a thread?
179+
So when should you spawn an `async` block, and when should you spawn a thread?
171180
- If you have `async` functions for your IO-bound operations at your disposal, feel free to spawn them on the main loop.
172181
- If your operation is computation-bound or there is no `async` function available, you have to spawn threads.

0 commit comments

Comments
 (0)