rust: 設定を共有するのは Arc らしい
特に難しいことをしたいつもりはなかった。
設定ファイルがあり、アプリの起動時に Config みたいな構造体に読み取り、
他のスレッドたちはそれぞれ参照するので書き換えることもないので持っておいてもらいたかっただけだ。
C言語だったら const Config* config のような変数に持たせておけばよいだけだったので軽く考えていたが、
Rust だとどうしたものだかよくわからんかった。
まず、参照を渡すというのが面倒だ。
実装している私はこの Config はアプリの最初から最後まで生きているのがわかっているのだが、コンパイラにはわかっていない。
例えばこう書くと tokio::spawn() の start(&config) で “config does not live long enough” “borrowed value does not live long enough!” になる。
// tokio = { version = "1.48.0", features = ["full", "rt-multi-thread"] }
use std::{
sync::Arc, thread, time::Duration,
};
use tokio::{self, sync::Notify};
async fn start(config: &Config) {
let mut count = config.init;
loop {
println!("count={}", count);
count = count + 1;
thread::sleep(Duration::from_secs(5));
}
}
struct Config {
init: i32,
}
#[tokio::main]
async fn main() {
let config = Config { init: 3 };
tokio::spawn(start(&config));
// 負荷無し待機(らしい)
let notify = Arc::new(Notify::new());
notify.notified().await;
}
アクセスする所有権は渡しても良いが中身の複製をしたくないときは Box<T> というのを最近見かけたので、それがこれだろうと思った。
Config の中身が i32 だけだと clone されたかどうかわからんので String も追加した。
use tokio::{self, sync::Notify};
async fn start(num: i32, config: Box::<Config>) {
println!("[{num}]config.message: {:p}", &config.message);
infinite_loop().await;
}
fn print(config: Box::<Config>) {
println!("config.message: {:p}", &config.message);
println!("init={}, message={}", config.init, config.message);
}
#[derive(Clone)]
struct Config {
init: i32,
message: String,
}
#[tokio::main]
async fn main() {
let config = Config { init: 3, message: "がんばれ".to_string() };
let config_share = Box::new(config);
print(Box::clone(&config_share));
tokio::spawn(start(0, Box::clone(&config_share)));
tokio::spawn(start(1, Box::clone(&config_share)));
infinite_loop().await;
}
// 負荷無し待機(らしい)
async fn infinite_loop() {
let notify = Box::new(Notify::new());
notify.notified().await;
}
実行すると Stirng のアドレスが変わっている。
ということはコピーされた? String の外側だけ作られて中身は流用? 区別が付かん。。。
print() でのアドレスと最初の start() でのアドレスが同じなのが気になるが、コピーが必要なかったので流用されたのか?
$ cargo run
config.message: 0x58c2433447b0
init=3, message=がんばれ
[0]config.message: 0x58c2433447b0
[1]config.message: 0x58c243344910
調べているともう1つ出てきたのが Arc。
今回は Gemini氏を使ったのだが、共有するなら Arc じゃろう、みたいな感じで紹介されたのだ。
Box:: を Arc:: に置換して use を追加しただけだ。
use std::sync::Arc;
use tokio::{self, sync::Notify};
async fn start(num: i32, config: Arc::<Config>) {
println!("[{num}]config.message: {:p}", &config.message);
infinite_loop().await;
}
fn print(config: Arc::<Config>) {
println!("config.message: {:p}", &config.message);
println!("init={}, message={}", config.init, config.message);
}
#[derive(Clone)]
struct Config {
init: i32,
message: String,
}
#[tokio::main]
async fn main() {
let config = Config { init: 3, message: "がんばれ".to_string() };
let config_share = Arc::new(config);
print(Arc::clone(&config_share));
tokio::spawn(start(0, Arc::clone(&config_share)));
tokio::spawn(start(1, Arc::clone(&config_share)));
infinite_loop().await;
}
// 負荷無し待機(らしい)
async fn infinite_loop() {
let notify = Arc::new(Notify::new());
notify.notified().await;
}
実行すると、こちらは全部同じアドレスになった。
$ cargo run
config.message: 0x589955819790
init=3, message=がんばれ
[0]config.message: 0x589955819790
[1]config.message: 0x589955819790
Arc は Atomic-Rc
Box は単一所有権が強制されるとあるので、複数で参照するということもできないと思っておけば良いのか。
そもそも所有権があるので複数で参照するという考え方が概念としてあわないのかしらね。
Arc が出てくるのはまあまあ後ろの方で 16.3章だ。
楕円などに由来するのかと思っていたのだが、a は atomic の “a” ということなので A-Rc となる。
Rc は Reference Counting の略で参照カウンタの略。
- 16.3 [Arc
で原子的な参照カウント](https://doc.rust-jp.rs/book-ja/ch16-03-shared-state.html?highlight=arc#arct%E3%81%A7%E5%8E%9F%E5%AD%90%E7%9A%84%E3%81%AA%E5%8F%82%E7%85%A7%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88) - 15.4 [Rc
は、参照カウント方式のスマートポインタ - The Rust Programming Language 日本語版](https://doc.rust-jp.rs/book-ja/ch15-04-rc.html)
- 15.4 [Rc
今回は参照するだけでカウントも不要なのだが、atomic である必要もないから Arc ではなく Rc でいけるかも。
そう思って書き換えたが tokio::spawn() で Rc::clone() しているところがダメそうだ。
Rc<Config> に Send トレイト境界がないのでスレッド間で安全に渡せないせいだと言っている。
AI(たぶん Gemini Code Assist) に修正お願いしたら Arc に書き換えられたので、もうそういうものだと思っておこう。