Background worker thread and synchronization in Rust - multithreading

I'm trying to write a simple library that has a background worker thread that processes commands when the library functions are called.
The way that I would normally do it in C is have a global semaphore handle that the worker would block on. The functions would give the semaphore after they sent a command, at which point the worker would unblock etc... There are other ways but this is just an example.
I have a few questions about how I can achieve something similar with Rust.
How do I prevent a thread from closing once the function that created it returns? e.g the thread would be created when I call init(), but would exit when init() returns, how to prevent this?
How to have a have a global synchronization method between the worker thread and function calls? I was looking at using channels but how do I access the rx from the thread and multiple tx's from different functions? e.g send_cmd_a(), send_cmd_b() to the same thread
Pseudo code of what I'm trying to accomplish:
static (tx, rx) = mpsc::channel(); //how to do something like this?
fn init() {
thread::spawn(|| {
loop {
let cmd = rx.recv().unwrap(); //blocks till there is data
//process data....
if cmd == "exit" {
return;
}
}
});
}
fn send_cmd_a() {
//Do a bunch of other stuff...
tx.send("cmd_a").unwrap();
}
fn exit() {
tx.send("exit").unwrap();
}
Do I have to create one big object that encapsulates all of this, thus owning the synchronization mechanism? (still doesn't answer question #1)
What would be the preferred way of doing something like this in Rust?

I think I figured out a way to implement what I wanted in Rust without needing to use global variables.
struct Device {
sender: Sender<u8>, //other stuff
}
trait New {
fn new() -> Self;
}
trait SendCommand {
fn send_command(&self, u8);
}
impl New for Device {
fn new() -> Device {
let (tx, rx) = channel();
let device = Device { sender: tx };
thread::spawn(move || {
loop {
let cmd = rx.recv().unwrap();
println!("Command: {}", cmd); //process commands here
}
});
return device;
}
}
impl SendCommand for Device {
fn send_command(&self, cmd: u8) {
self.sender.send(cmd).unwrap();
}
}
fn main() {
let dev = Device::new(); //create the device
for i in 0..10 {
dev.send_command(i); //send commands
sleep(Duration::from_millis(50));
}
loop {}
}

Related

Separate thread for loop in struct implementation

I'm working with a struct where I need to read the GPIO pin of a Raspberry Pi, and increment a 'register' within the struct every time the pin goes high. Concurrently with this, I would like to be able to sample the register every now and then to see what the current value is.
When implementing this, my thought was to spawn a thread that continuously loops checking if the pin has gone from Low to High, and increment the register from within the thread. Then, from the parent thread, I can read the value of the register and report it.
After doing some research, it seems that a scoped thread would not be the correct implementation of this, because the child thread would never hand over ownership of the register to the parent thread.
Rather, I believe I should use an Arc/Mutex combination guarding the register and only momentarily take control over the lock to increment the register. Is this the correct interpretation of multithreading in Rust?
Assuming the above is correct, I'm unsure of how to implement this in Rust.
struct GpioReader {
register: Arc<Mutex<i64>>,
input_pin: Arc<Mutex<InputPin>>,
}
impl GpioReader {
pub fn new(input_pin: InputPin) -> Self {
Self {
register: Arc::New(Mutex::from(0)),
input_pin: Arc::new(Mutex::from(input_pin))
}
}
pub fn start(&self) {
let pin = self.input_pin.lock().unwrap(); // ???
let register = self.register.lock().unwrap(); // ???
let handle = spawn(move || loop {
match pin.read() { // ???
High => register += 1, // ???
Low => (),
}
sleep(Duration::from_millis(SLEEP_TIME));
});
handle.join().expect("Failed to join thread.");
}
pub fn get_register(&self) -> i64 {
let reg_val = self.register.lock().unwrap();
return reg_val;
}
}
Given the above, how do I declare the pin and register variables in such a way that I can read off the pin and increment the register within the loop? My best guess is I'll have to instantiate some kind of reference to these members of the struct outside of the loop, and then pass the reference into the loop at which point I can use the lock() method of the Arc.
Edit: Using RaspberryPi 3A+ running Raspbian. The InputPin in question is from the rppal crate.
Mutex<i64> is an anti-pattern. Replace it with AtomicI64.
Arc is meant to be cloned with Arc::clone() to create new references to the same object.
Don't use shared ownership if not necessary. InputPin is only used from within the thread, so move it in instead.
I'm unsure why you do handle.join(). If you want it to continue in the background, don't wait for it with .join().
use std::{
sync::{
atomic::{AtomicI64, Ordering},
Arc,
},
thread::{self, sleep},
time::Duration,
};
use rppal::gpio::InputPin;
struct GpioReader {
register: Arc<AtomicI64>,
input_pin: Option<InputPin>,
}
const SLEEP_TIME: Duration = Duration::from_millis(1000);
impl GpioReader {
pub fn new(input_pin: InputPin) -> Self {
Self {
register: Arc::new(AtomicI64::new(0)),
input_pin: Some(input_pin),
}
}
pub fn start(&mut self) {
let register = Arc::clone(&self.register);
let pin = self.input_pin.take().expect("Thread already running!");
let handle = thread::spawn(move || loop {
match pin.read() {
High => {
register.fetch_add(1, Ordering::Relaxed);
}
Low => (),
}
sleep(SLEEP_TIME);
});
}
pub fn get_register(&self) -> i64 {
self.register.load(Ordering::Relaxed)
}
}
If you want to stop the thread automatically when the GpioReader object is dropped, you can use Weak to signal it to the thread:
use std::{
sync::{
atomic::{AtomicI64, Ordering},
Arc,
},
thread::{self, sleep},
time::Duration,
};
use rppal::gpio::InputPin;
struct GpioReader {
register: Arc<AtomicI64>,
input_pin: Option<InputPin>,
}
const SLEEP_TIME: Duration = Duration::from_millis(1000);
impl GpioReader {
pub fn new(input_pin: InputPin) -> Self {
Self {
register: Arc::new(AtomicI64::new(0)),
input_pin: Some(input_pin),
}
}
pub fn start(&mut self) {
let register = Arc::downgrade(&self.register);
let pin = self.input_pin.take().expect("Thread already running!");
let handle = thread::spawn(move || loop {
if let Some(register) = register.upgrade() {
match pin.read() {
High => {
register.fetch_add(1, Ordering::Relaxed);
}
Low => (),
}
sleep(SLEEP_TIME);
} else {
// Original `register` got dropped, cancel the thread
break;
}
});
}
pub fn get_register(&self) -> i64 {
self.register.load(Ordering::Relaxed)
}
}

How can I share a Vector between 2 threads?

I am pretty new to Rust, and cannot manage to keep both Arcs values updated in both threads I'm spawning. The idea would be that one thread loops over received events and when it receives one, updates the object, which the other thread constantly watches. How can I achieve that in Rust, or if this method isn't adequate, would there be a better way to do it ?
(The concrete idea would be one thread listening for MIDI events and the other one re-rendering on a LED strip the notes received)
Here's what I currently have:
main.rs
mod functions;
mod structs;
use crate::functions::*;
use crate::structs::*;
use portmidi as pm;
use rs_ws281x::{ChannelBuilder, ControllerBuilder, StripType};
use std::sync::{Arc, Mutex};
use std::{fs, thread, time};
const MIDI_TIMEOUT: u64 = 10;
const MIDI_CHANNEL: usize = 0;
#[tokio::main]
async fn main() {
let config: Arc<std::sync::Mutex<Config>> = Arc::new(Mutex::new(
toml::from_str(&fs::read_to_string("config.toml").unwrap()).unwrap(),
));
let config_midi = config.clone();
let config_leds = config.clone();
let leds_status = Arc::new(Mutex::new(vec![0; config.lock().unwrap().leds.num_leds]));
let leds_status_midi = Arc::clone(&leds_status);
let leds_status_leds = Arc::clone(&leds_status);
thread::spawn(move || {
let config = config_midi.lock().unwrap();
let midi_context = pm::PortMidi::new().unwrap();
let device_info = midi_context
.device(config.midi.id)
.expect(format!("Could not find device with id {}", config.midi.id).as_str());
println!("Using device {}) {}", device_info.id(), device_info.name());
let input_port = midi_context
.input_port(device_info, config.midi.buffer_size)
.expect("Could not create input port");
let mut leds_status = leds_status_midi.lock().unwrap();
loop {
if let Ok(_) = input_port.poll() {
if let Ok(Some(events)) = input_port.read_n(config.midi.buffer_size) {
for event in events {
let event_type =
get_midi_event_type(event.message.status, event.message.data2);
match event_type {
MidiEventType::NoteOn => {
let key = get_note_position(event.message.data1, &config);
leds_status[key] = 1;
}
MidiEventType::NoteOff => {
let key = get_note_position(event.message.data1, &config);
leds_status[key] = 0;
}
_ => {}
}
}
}
}
thread::sleep(time::Duration::from_millis(MIDI_TIMEOUT));
}
});
thread::spawn(move || {
let config = config_leds.lock().unwrap();
let mut led_controller = ControllerBuilder::new()
.freq(800_000)
.dma(10)
.channel(
MIDI_CHANNEL,
ChannelBuilder::new()
.pin(config.leds.pin)
.count(config.leds.num_leds as i32)
.strip_type(StripType::Ws2812)
.brightness(config.leds.brightness)
.build(),
)
.build()
.unwrap();
loop {
let leds_status = leds_status_leds.lock().unwrap();
print!("\x1b[2J\x1b[1;1H");
println!(
"{:?}",
leds_status.iter().filter(|x| (**x) > 0).collect::<Vec<_>>()
);
}
});
}
functions.rs
use crate::structs::MidiEventType;
pub fn get_note_position(note: u8, config: &crate::structs::Config) -> usize {
let mut note_offset = 0;
for i in 0..config.leds.offsets.len() {
if note > config.leds.offsets[i][0] {
note_offset = config.leds.offsets[i][1];
break;
}
}
note_offset -= config.leds.shift;
let note_pos_raw = 2 * (note - 20) - note_offset;
config.leds.num_leds - (note_pos_raw as usize)
}
pub fn get_midi_event_type(status: u8, velocity: u8) -> MidiEventType {
if status == 144 && velocity > 0 {
MidiEventType::NoteOn
} else if status == 128 || (status == 144 && velocity == 0) {
MidiEventType::NoteOff
} else {
MidiEventType::ControlChange
}
}
structs.rs
use serde_derive::Deserialize;
#[derive(Deserialize, Debug)]
pub struct Config {
pub leds: LedsConfig,
pub midi: MidiConfig,
}
#[derive(Deserialize, Debug)]
pub struct LedsConfig {
pub pin: i32,
pub num_leds: usize,
pub brightness: u8,
pub offsets: Vec<Vec<u8>>,
pub shift: u8,
pub fade: i8,
}
#[derive(Deserialize, Debug)]
pub struct MidiConfig {
pub id: i32,
pub buffer_size: usize,
}
#[derive(Debug)]
pub enum MidiEventType {
NoteOn,
NoteOff,
ControlChange,
}
Thank you very much !
The idea would be that one thread loops over received events and when it receives one, updates the object, which the other thread constantly watches.
That's a good way to do it, particularly if one of the threads needs to be near-realtime (e.g. live audio processing). You can use channels to achieve this. You transfer the sender to one thread and the receiver to another. In a realtime scenario, the receiver can loop until try_recv errs with Empty (limiting to some number of iterations to prevent starvation of the processing code). For example, something like this, given a r: Receiver:
// Process 100 messages max to not starve the thread of the other stuff
// it needs to be doing.
for _ in 0..100 {
match r.try_recv() {
Ok(msg) => { /* Process msg, applying it to the current state */ },
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
// The sender is gone, maybe this is our signal to terminate?
return;
},
}
}
Alternatively, if one thread needs to act only when a message is received, it can simply iterate the receiver, which will continue to loop as long as messages are received and the channel is open:
for msg in r {
// Handle the message
}
It really is that simple. If the channel is empty but there are senders alive, it will block until a message is received. Once all senders are gone and the channel is empty, the loop will terminate.
A channel can convey messages of exactly one type; if only one kind of message needs to be sent, you can use a struct. Otherwise, an enum with variants for each kind of message works well.
Given the sending side of the channel, s: Sender, you just s.send(your_message_value).
Another option would be to create an Arc<Mutex<_>>, which it looks like you are doing in your sample code. This way is fine if the lock contention is not too high, but this can inhibit the ability of both threads to run concurrently, which is often the goal of multithreading. Channels tend to work better in message-passing scenarios because there isn't a need for a mutual exclusion lock.
As a side note, you are using Tokio with an async main(), but you never actually do anything with any futures, so there's no reason to even use Tokio in this code.

Avoid deadlock in rust when multiple spawns execute code in a loop

I am trying to run 2 threads in parallel and share some data between them. When either one of the threads contain a loop statement, the shared data in the other thread goes into a deadlock.
But if I were to add a line to code to break out of the loop statement after a certain number of iterations, the deadlock gets released and the operation in the next thread starts.
Rust Playground
Code:
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
#[derive(Clone, Copy)]
struct SomeNetwork {
is_connected: bool,
}
impl SomeNetwork {
fn connection_manager(&mut self) {
loop {
// if I exit the loop after a few iterations then the deadlock is removed
// eg: when I use `for i in 0..10 {` instead of `loop`
println!("connection_manager thread...");
thread::sleep(Duration::from_millis(2000));
}
}
fn api_calls(&self) {
loop {
if self.is_connected {
//make_an_api_call()
}
println!("api_calls thread...");
thread::sleep(Duration::from_millis(5000));
}
}
pub fn start() {
let self_arc = SomeNetwork {
is_connected: false,
};
let self_arc = Arc::new(Mutex::new(self_arc));
let self_cloned1 = Arc::clone(&self_arc);
let self_cloned2 = Arc::clone(&self_arc);
thread::Builder::new()
.spawn(move || {
let mut n = self_cloned1.lock().unwrap();
n.connection_manager();
})
.unwrap();
thread::Builder::new()
.spawn(move || {
let n = self_cloned2.lock().unwrap(); // <---- deadlock here
n.api_calls();
})
.unwrap();
loop {
thread::sleep(Duration::from_millis(5000))
}
}
}
fn main() {
SomeNetwork::start();
}
Output:
connection_manager thread...
connection_manager thread...
connection_manager thread...
connection_manager thread...
connection_manager thread...
....
Wouldn't the underlying OS take care of the scheduling once a thread goes into sleep?
What could be done here, so that I can run both threads in parallel?
The issue is the mutex you created stays locked during connection_manager.
The way you use a mutex in Rust is that it wraps the data it locks. When you lock the mutex, it blocks the current thread until it can obtain the mutex. Once it has, it gives you a MutexGuard which you can think of as a wrapper for a reference to the mutex. The MutexGuard gives you mutable access to the data inside the mutex. Then once the MutexGuard is no longer needed Rust invokes MutexGuard's implementation of Drop which unlocks the mutex and allows other threads to obtain it.
// Block until mutex is locked for this thread and return MutexGuard
let mut n = self_cloned1.lock().unwrap();
// Do stuff with the locked mutex
n.connection_manager();
// MutexGuard is no longer needed so it gets dropped and the mutex is released
As you can see, if connection_manager never exits the mutex will remain locked for the first thread to obtain the mutex.
What you want is probably to use a mutex with a condvar so the mutex can be released while the thread is sleeping.
Edit:
Here is a rough idea of what that using condvars to handle connecting and channels to pass jobs to workers would look like. Playground Link
use std::sync::{Arc, Mutex, Condvar};
use std::thread::{self, current};
use std::time::Duration;
use crossbeam_channel::{unbounded, Receiver};
#[derive(Clone, Copy)]
struct SomeNetwork {
is_connected: bool,
}
const TIMEOUT: Duration = Duration::from_secs(5);
impl SomeNetwork {
fn connect(&mut self) {
println!("connection_manager thread...");
self.is_connected = true;
}
fn api_calls(&self, job: i32) {
//println!("api_calls thread...");
println!("[Worker {:?}] Handling job {}", current().id(), job);
thread::sleep(Duration::from_millis(50))
}
pub fn start_connection_thread(
self_data: Arc<Mutex<Self>>,
connect_condvar: Arc<Condvar>,
worker_condvar: Arc<Condvar>,
) {
thread::Builder::new()
.spawn(move || {
let mut guard = self_data.lock().unwrap();
loop {
// Do something with the data
if !guard.is_connected {
guard.connect();
// Notify all workers that the connection is ready
worker_condvar.notify_all();
}
// Use condvar to release mutex and wait until signaled to start again
let (new_guard, _) = connect_condvar.wait_timeout(guard, TIMEOUT).unwrap();
guard = new_guard;
}
})
.unwrap();
}
pub fn start_worker_thread(
self_data: Arc<Mutex<Self>>,
connect_condvar: Arc<Condvar>,
worker_condvar: Arc<Condvar>,
requests: Receiver<i32>,
) {
thread::Builder::new()
.spawn(move || {
loop {
// Wait until a request is received
let request = requests.recv().unwrap();
// Lock mutex once we have a request
let mut guard = self_data.lock().unwrap();
// Make sure we are connected before starting tasks
while !guard.is_connected {
// Wake up 1 connection thread if the connection breaks
connect_condvar.notify_one();
// Sleep until signaled that the connection has been fixed
let (new_guard, _) = worker_condvar.wait_timeout(guard, TIMEOUT).unwrap();
guard = new_guard;
}
// Now that we have verified we are connected, handle the request
guard.api_calls(request);
}
})
.unwrap();
}
pub fn start() {
let self_arc = SomeNetwork {
is_connected: false,
};
let self_arc = Arc::new(Mutex::new(self_arc));
let connect_condvar = Arc::new(Condvar::new());
let worker_condvar = Arc::new(Condvar::new());
// Create a channel to send jobs to workers
let (send, recv) = unbounded();
Self::start_connection_thread(self_arc.clone(), connect_condvar.clone(), worker_condvar.clone());
// Start some workers
for _ in 0..5 {
Self::start_worker_thread(self_arc.clone(), connect_condvar.clone(), worker_condvar.clone(), recv.clone());
}
// Send messages to workers
for message in 1..100 {
send.send(message);
}
loop {
thread::sleep(Duration::from_millis(5000))
}
}
}
fn main() {
SomeNetwork::start();
}

How to connect bevy game to externel TCP server using tokios async TcpStream?

I want to send Events between the game client and server and I already got it working, but I do not know how to do it with bevy.
I am dependent to use tokios async TcpStream, because I have to be able to split the stream into a OwnedWriteHalf and OwnedReadhalf using stream.into_split().
My first idea was to just spawn a thread that handles the connection and then send the received events to a queue using mpsc::channel
Then I include this queue into a bevy resource using app.insert_resource(Queue) and pull events from it in the game loop.
the Queue:
use tokio::sync::mpsc;
pub enum Instruction {
Push(GameEvent),
Pull(mpsc::Sender<Option<GameEvent>>),
}
#[derive(Clone, Debug)]
pub struct Queue {
sender: mpsc::Sender<Instruction>,
}
impl Queue {
pub fn init() -> Self {
let (tx, rx) = mpsc::channel(1024);
init(rx);
Self{sender: tx}
}
pub async fn send(&self, event: GameEvent) {
self.sender.send(Instruction::Push(event)).await.unwrap();
}
pub async fn pull(&self) -> Option<GameEvent> {
println!("new pull");
let (tx, mut rx) = mpsc::channel(1);
self.sender.send(Instruction::Pull(tx)).await.unwrap();
rx.recv().await.unwrap()
}
}
fn init(mut rx: mpsc::Receiver<Instruction>) {
tokio::spawn(async move {
let mut queue: Vec<GameEvent> = Vec::new();
loop {
match rx.recv().await.unwrap() {
Instruction::Push(ev) => {
queue.push(ev);
}
Instruction::Pull(sender) => {
sender.send(queue.pop()).await.unwrap();
}
}
}
});
}
But because all this has to be async I have block the pull() function in the sync game loop.
I do this using the futures-lite crate:
fn event_pull(
communication: Res<Communication>
) {
let ev = future::block_on(communication.event_queue.pull());
println!("got event: {:?}", ev);
}
And this works fine, BUT after around 5 seconds the whole program just halts and does not receive any more events.
It seems like that future::block_on() does block indefinitely.
Having the main function, in which bevy::prelude::App gets built and run, to be the async tokio::main function might also be a problem here.
It would probably be best to wrap the async TcpStream initialisation and tokio::sync::mpsc::Sender and thus also Queue.pull into synchronous functions, but I do not know how to do this.
Can anyone help?
How to reproduce
The repo can be found here
Just compile both server and client and then run both in the same order.
I got it to work by just replacing every tokio::sync::mpsc with crossbeam::channel, which might be a problem, as it does block
and manually initializing the tokio runtime.
so the init code looks like this:
pub struct Communicator {
pub event_bridge: bridge::Bridge,
pub event_queue: event_queue::Queue,
_runtime: Runtime,
}
impl Communicator {
pub fn init(ip: &str) -> Self {
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_io()
.build()
.unwrap();
let (bridge, queue, game_rx) = rt.block_on(async move {
let socket = TcpStream::connect(ip).await.unwrap();
let (read, write) = socket.into_split();
let reader = TcpReader::new(read);
let writer = TcpWriter::new(write);
let (bridge, tcp_rx, game_rx) = bridge::Bridge::init();
reader::init(bridge.clone(), reader);
writer::init(tcp_rx, writer);
let event_queue = event_queue::Queue::init();
return (bridge, event_queue, game_rx);
});
// game of game_rx events to queue for game loop
let eq_clone = queue.clone();
rt.spawn(async move {
loop {
let event = game_rx.recv().unwrap();
eq_clone.send(event);
}
});
Self {
event_bridge: bridge,
event_queue: queue,
_runtime: rt,
}
}
}
And main.rs looks like this:
fn main() {
let communicator = communication::Communicator::init("0.0.0.0:8000");
communicator.event_bridge.push_tcp(TcpEvent::Register{name: String::from("luca")});
App::new()
.insert_resource(communicator)
.add_system(event_pull)
.add_plugins(DefaultPlugins)
.run();
}
fn event_pull(
communication: Res<communication::Communicator>
) {
let ev = communication.event_queue.pull();
if let Some(ev) = ev {
println!("ev");
}
}
Perhaps there might be a better solution.

gtk-rs: how to update view from another thread

I am creating a UI application with gtk-rs. In that application, I have to spawn a thread to continuously communicate with another process. Sometimes, I have to update the UI based on what happens in that thread. But, I'm not sure how to do this because I am not able to hold a reference to any part of the UI across threads.
Here is the code I tried:
use gtk;
fn main() {
let application =
gtk::Application::new(Some("com.github.gtk-rs.examples.basic"), Default::default()).unwrap()
application.connect_activate(|app| {
let ui_model = build_ui(app);
setup(ui_model);
});
application.run(&[]);
}
struct UiModel { main_buffer: gtk::TextBuffer }
fn build_ui(application: &gtk::Application) -> UiModel {
let glade_src = include_str!("test.glade");
let builder = gtk::Builder::new();
builder
.add_from_string(glade_src)
.expect("Couldn't add from string");
let window: gtk::ApplicationWindow = builder.get_object("window").unwrap();
window.set_application(Some(application));
window.show_all();
let main_text_view: gtk::TextView = builder.get_object("main_text_view")
return UiModel {
main_buffer: main_text_view.get_buffer().unwrap(),
};
}
fn setup(ui: UiModel) {
let child_process = Command::new("sh")
.args(&["-c", "while true; do date; sleep 2; done"])
.stdout(Stdio::piped())
.spawn()
.unwrap();
let incoming = child_process.stdout.unwrap();
std::thread::spawn(move || { // <- This is the part to pay
&BufReader::new(incoming).lines().for_each(|line| { // attention to.
ui.main_buffer.set_text(&line.unwrap()); // I am trying to update the
}); // UI text from another thread.
});
}
But, I get the error:
| std::thread::spawn(move || {
| _____^^^^^^^^^^^^^^^^^^_-
| | |
| | `*mut *mut gtk_sys::_GtkTextBufferPrivate` cannot be sent between threads safely
This makes sense. I can understand that the Gtk widgets aren't thread safe. But then how do I update them? Is there a way to send signals to the UI thread safely? or is there a way to run the .lines().for_each( loop in the same thread in a way that does not block the UI?
Whatever solution I go with will have to be very high performance. I will be sending much more data than in the example and I want a very low latency screen refresh.
Thanks for your help!
Ok, I solved the problem. For anyone in the future, here is the solution.
glib::idle_add(|| {}) lets you run a closure from another thread on the UI thread (thansk #Zan Lynx). This would be enough to solve the thread safety issue, but it's not enough to get around the borrow checker. No GTKObject is safe to send between threads, so another thread can never even hold a reference to it, even if it will never use it. So you need to store the UI references globally on the UI thread and set up a communication channel between threads. Here is what I did step by step:
Create a way to send data between threads that does not involve passing closures. I used std::sync::mpsc for now but another option might be better long-term.
Create some thread-local global storage. Before you ever start the second thread, store your UI references and the receiving end of that communication pipeline globally on the main thread.
Pass the sending end of the channel to the second thread via a closure. Pass the data you want through that sender.
After passing the data through, use glib::idle_add() -- not with a closure but with a static function -- to tell the UI thread to check for a new message in the channel.
In that static function on the UI thread, access your global UI and receiver variables and update the UI.
Thanks to this thread for helping me figure that out. Here is my code:
extern crate gio;
extern crate gtk;
extern crate pango;
use gio::prelude::*;
use gtk::prelude::*;
use std::cell::RefCell;
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
use std::sync::mpsc;
fn main() {
let application =
gtk::Application::new(Some("com.github.gtk-rs.examples.basic"), Default::default())
.unwrap();
application.connect_activate(|app| {
let ui_model = build_ui(app);
setup(ui_model);
});
application.run(&[]);
}
struct UiModel {
main_buffer: gtk::TextBuffer,
}
fn build_ui(application: &gtk::Application) -> UiModel {
let glade_src = include_str!("test.glade");
let builder = gtk::Builder::new();
builder
.add_from_string(glade_src)
.expect("Couldn't add from string");
let window: gtk::ApplicationWindow = builder.get_object("window").unwrap();
window.set_application(Some(application));
window.show_all();
let main_text_view: gtk::TextView = builder.get_object("main_text_view").unwrap();
return UiModel {
main_buffer: main_text_view.get_buffer().unwrap(),
};
}
fn setup(ui: UiModel) {
let (tx, rx) = mpsc::channel();
GLOBAL.with(|global| {
*global.borrow_mut() = Some((ui, rx));
});
let child_process = Command::new("sh")
.args(&["-c", "while true; do date; sleep 2; done"])
.stdout(Stdio::piped())
.spawn()
.unwrap();
let incoming = child_process.stdout.unwrap();
std::thread::spawn(move || {
&BufReader::new(incoming).lines().for_each(|line| {
let data = line.unwrap();
// send data through channel
tx.send(data).unwrap();
// then tell the UI thread to read from that channel
glib::source::idle_add(|| {
check_for_new_message();
return glib::source::Continue(false);
});
});
});
}
// global variable to store the ui and an input channel
// on the main thread only
thread_local!(
static GLOBAL: RefCell<Option<(UiModel, mpsc::Receiver<String>)>> = RefCell::new(None);
);
// function to check if a new message has been passed through the
// global receiver and, if so, add it to the UI.
fn check_for_new_message() {
GLOBAL.with(|global| {
if let Some((ui, rx)) = &*global.borrow() {
let received: String = rx.recv().unwrap();
ui.main_buffer.set_text(&received);
}
});
}

Resources