6

I'm trying to use the thread-local LocalKey to have a global game variable that the user can set once at the start of playing.

I finally got it to compile while setting a new PLAYER_NAME in a with block:

use std::thread::LocalKey;
use std::borrow::BorrowMut;

thread_local! {
    pub static PLAYER_NAME: String = String::from("player-one");
}

fn main() {
    let p: String = String::from("new-name");

    PLAYER_NAME.with(|mut player_name| {
        let player_name = p;
    });

    println!("PLAYER_NAME is: {:?}", PLAYER_NAME);
}

This prints out:

PLAYER_NAME is: LocalKey { .. }

How do I print the string value of PLAYER_NAME? Do I have to use a with block every time I want to read it too?

2 Answers 2

12

Do I have to use a with block every time I want to read it too?

Yes if you are accessing PLAYER_NAME directly - see @Shepmaster's answer for an example. But what you'd typically do in a real program is encapsulate the access to the global in functions, which buy you the usage pattern you know from other languages without loss of performance or convenience. For example:

use std::cell::RefCell;

thread_local! {
    pub static PLAYER_NAME: RefCell<String>
        = RefCell::new("player-one".to_string());
}

fn set_player_name(name: String) {
    PLAYER_NAME.with(|player_name| {
        *player_name.borrow_mut() = name
    });
}

fn get_player_name() -> String {
    PLAYER_NAME.with(|player_name| player_name.borrow().clone())
}

fn main() {
    assert_eq!(get_player_name(), "player-one".to_string());
    set_player_name("mini me".to_string());
    assert_eq!(get_player_name(), "mini me".to_string());
}

If you don't want the player name to be per-thread, then replace thread_local! with lazy_static!, RefCell with a RwLock, borrow() with read(), borrow_mut() with write(), and you will no longer need the with.

Sign up to request clarification or add additional context in comments.

5 Comments

without loss of performance — I'm not sure I follow you; there's still the underlying performance hit of checking if the variable is initialized and initializing it if not, why do you say that this loss of performance is negated? If you don't want the player name to be per-thread — seems like thread_local! doesn't really fit with that concept.
@Shepmaster Re performance, I was referring to the fact that I expect the compiler to inline the call, so the effect should be the same as the with. (Technically my proposed getter invokes clone so it's slower than pure with, but the code inside the with would likely also clone the string to get it outside.)
@Shepmaster Re thread-local, the idea was not to use thread_local! in that case, which the answer neglected to actually mention. Fixed now, thanks.
The performance is also made worse by the fact that you can't return a reference to the string. get_player_name is forced to clone.
@JosephGarvin If you're worried about performance of the string clone, you can easily make get_player_name() return Rc<String> instead. As mentioned in previous comments, "no loss of performance" refers to PLAYER_NAME.with() in get_player_name() being inlinable by the compiler and therefore a zero-cost abstraction.
4

How do I print the string value of PLAYER_NAME? Do I have to use a with block every time I want to read it too?

Yes. The compiler has no way of knowing which arbitrary call to PLAYER_NAME would be the first one and which would come after. Every time you access the thread-local, it has to be checked to ensure that it's been initialized and do so if it hasn't. with performs that check.


Beyond that, you have a number of other issues. Rust is a compiled language, which means you should listen to the warnings it prints.

let player_name = p;

This declares a new variable that shadows the closure variable player_name, it does not set it.

You are then attempting to mutate an immutable reference, which cannot work. You'll need some kind of internal mutability, such as RefCell.

use std::cell::RefCell;

thread_local! {
    pub static PLAYER_NAME: RefCell<String> = RefCell::new(String::from("player-one"));
}

fn main() {
    let p: String = String::from("new-name");

    PLAYER_NAME.with(|player_name| {
        *player_name.borrow_mut() = p;
    });

    PLAYER_NAME.with(|player_name| {
        println!("The name is: {}", player_name.borrow());
    });
}

See also:


I'd also strongly encourage you to just try and use standard Rust references and pass values down from a parent context. It's usually a lot easier to understand than some magical semi-global state, especially if you are new to Rust.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.