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?

nabanino
  • 63
  • 1
  • 4

2 Answers2

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.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • *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 Oct 21 '17 at 20:28
  • @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.) – user4815162342 Oct 21 '17 at 21:24
  • @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. – user4815162342 Oct 21 '17 at 21:25
  • 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. – Joseph Garvin Aug 22 '20 at 18:08
  • @JosephGarvin If you're worried about performance of the string clone, you can easily make `get_player_name()` [return `Rc` instead](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d0e4a8af88e39ef8ac9b22de75abdeaf). 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. – user4815162342 Aug 22 '20 at 18:25
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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366