どうも、ryo_gridです。
先日、以下のような記事を書きました。
Pythonで書いた分散KVSのシミュレータをRustに書き換えようとして苦労している話 - Qiita
その記事の終わりに(少なくとも)以下のような課題が残っているという旨を書きました。
・HashMapの場合、どう書けばよいのかよく分からないなーという感じで悩み中です
・get_first_data関数は、本当は関数内でロックをとってデータを返すということをしたかったのですが、Error : returns a value referencing data owned by the current function というエラーが解決できず、引数で必要なデータを渡す形になりました。lockして得たデータのスコープを考えれば当たり前なのかもしれませんが、この手の関数を書くときにいちいちlockしたデータへの参照を渡すのも煩わしいなあという感じで、私の知識不足であることを祈っています
これらについて、どのように記述すれば良いか分かったので補足の意味も込めてこの記事で整理しておきます。
実装
まず、コードを示します。
前回の記事のものに多少の修正とコードの追加を行っています。
※Rustには型推論があるため、変数の側に型を明示しなくても良い場面も多いですが、下のコードでは何型が代入されるか分かるように型を明に記述しています
.
├── Cargo.toml
├── src
│ ├── main.rs
│ ├── gval.rs
[package]
name = "rust_mt_example"
version = "0.0.2"
authors = ["Ryo Kanbayashi <ryo.contact@gmail.com>"]
edition = "2018"
[dependencies]
lazy_static = "1.4.0"
parking_lot = "0.11"
use std::sync::{Arc};
use std::cell::RefCell;
use std::collections::HashMap;
use parking_lot::{ReentrantMutex, const_reentrant_mutex};
pub struct GlobalDatas {
pub all_data_list : Vec<Arc<ReentrantMutex<RefCell<KeyValue>>>>,
pub all_data_dict : HashMap<String, Arc<ReentrantMutex<RefCell<KeyValue>>>>,
}
impl GlobalDatas {
pub fn new() -> GlobalDatas {
GlobalDatas {all_data_list : Vec::new(), all_data_dict : HashMap::new()}
}
}
lazy_static! {
pub static ref GLOBAL_DATAS : Arc<ReentrantMutex<RefCell<GlobalDatas>>> = Arc::new(const_reentrant_mutex(RefCell::new(GlobalDatas::new())));
}
#[derive(Debug, Clone)]
pub struct KeyValue {
pub key : Option<String>,
pub value_data : String,
pub data_id : Option<i32>
}
impl KeyValue {
pub fn new(key : Option<String>, value : String) -> KeyValue {
let tmp_data_id : Option<i32> = match &key {
Some(key_string) => Some(hash_str_to_int(&key_string)),
None => None
};
KeyValue {key : key, value_data : value, data_id : tmp_data_id}
}
}
pub fn hash_str_to_int(_input_str : &String) -> i32 {
//return fixed value
return 1000
}
#[macro_use] extern crate lazy_static;
pub mod gval;
pub use crate::gval::*;
use std::{borrow::BorrowMut, sync::{Arc}};
use std::cell::{RefMut, RefCell};
use parking_lot::{ReentrantMutex, const_reentrant_mutex};
fn get_first_data_no_arg() -> Arc<ReentrantMutex<RefCell<gval::KeyValue>>> {
let gd_refcell : &RefCell<GlobalDatas> = &*gval::GLOBAL_DATAS.lock();
let gd_refmut : &mut GlobalDatas = &mut gd_refcell.borrow_mut();
let kv_arc : Arc<ReentrantMutex<RefCell<KeyValue>>> = gd_refmut.all_data_list.get(0).unwrap().clone();
return Arc::clone(&kv_arc);
}
fn get_node_from_map(key: &String) -> Arc<ReentrantMutex<RefCell<gval::KeyValue>>>{
let gd_refcell : &RefCell<GlobalDatas> = &*gval::GLOBAL_DATAS.lock();
let gd_refmut : &GlobalDatas = &gd_refcell.borrow_mut();
let kv_arc : Arc<ReentrantMutex<RefCell<KeyValue>>> = gd_refmut.all_data_dict.get(key).unwrap().clone();
return Arc::clone(&kv_arc);
}
fn main() {
// Vecに関する処理を行うブロック
{
let locked_gd : &RefCell<GlobalDatas> = &*gval::GLOBAL_DATAS.lock();
{
let locked_gd_mut : &mut GlobalDatas = &mut locked_gd.borrow_mut();
//Vecに一つだけデータを追加する
locked_gd_mut.all_data_list.push(Arc::new(const_reentrant_mutex(RefCell::new(KeyValue::new(Some("ryo_grid".to_string()),"pythonista".to_string())))));
//ここで locked_gd_mutの参照は無効となるはず
}
let first_elem : Arc<ReentrantMutex<RefCell<gval::KeyValue>>>;
{
first_elem = get_first_data_no_arg();
let first_elem_tmp : &RefCell<KeyValue> = &*first_elem.as_ref().borrow_mut().lock();
let first_elem_to_print : &mut RefMut<KeyValue> = &mut first_elem_tmp.borrow_mut();
//この時点でのVec内唯一の要素をprintlnする
println!("{:?}", first_elem_to_print);
//ここで参照である first_elem_tmp や first_elem_to_print は無効となるはず
}
// first_elem変数に逃がしておいたArc型の値の中身をあれこれしてKeyValue型の
// mutableな参照を得る
let locked_elem : &RefCell<KeyValue> = &*first_elem.as_ref().borrow_mut().lock();
let locked_elem_mut : &mut RefMut<KeyValue> = &mut locked_elem.borrow_mut();
// Vec内に上で追加した要素を可変参照を介して変更する
locked_elem_mut.value_data = "Rustacean".to_string();
// 変更された要素をprintlnする
println!("{:?}", locked_elem_mut);
// ここで参照である locked_elem、lecked_elem_mut は無効となり、locked_elemに参照を代入
// する際に獲得した GLOBAL_DATAS のロックも解放されるはず
// また、同様に、locked_gd に参照を代入する際に獲得した GLOBAL_DATAS のロックも解放されるはず
// (reentrant なロックを用いているため、同一のデータに対して獲得した2つのロックが解放されるという理解で良い
// のだろうか・・・)
}
// 以降は HashMap に関連する処理を行っている
let refcell_gd : &RefCell<GlobalDatas> = &*gval::GLOBAL_DATAS.lock();
{
let mutref_gd : &mut GlobalDatas = &mut refcell_gd.borrow_mut();
mutref_gd.all_data_dict.insert(
"ryo_grid".to_string(),
Arc::new(
const_reentrant_mutex(RefCell::new(KeyValue::new(Some("rust".to_string()),"before_mod".to_string())))
)
);
// ここで参照である mutref_gd は無効となるはず
}
let one_elem : Arc<ReentrantMutex<RefCell<gval::KeyValue>>>;
{
one_elem = get_node_from_map(&"ryo_grid".to_string());
let one_elem_tmp : &RefCell<KeyValue> = &*one_elem.as_ref().borrow_mut().lock();
let one_elem_to_print : &mut RefMut<KeyValue> = &mut one_elem_tmp.borrow_mut();
println!("{:?}", one_elem_to_print);
// ここで参照である one_elem_tmp と one_elem_to_print は無効となるはず
}
let refcell_kv : &RefCell<KeyValue> = &*one_elem.as_ref().borrow_mut().lock();
let mutref_kv : &mut RefMut<KeyValue> = &mut refcell_kv.borrow_mut();
mutref_kv.value_data = "after_mod".to_string();
println!("{:?}", mutref_kv);
// 一応、ここで参照である refcell_gd、refcell_kv、mutref_kv が無効となり、
// refcell_gd に参照を設定するために獲得した GlobalDatas のロックが解放され、
// Arc型の値である one_elem が無効となるはず
}
例のごとく、repl.it のスニペットも用意しておきました。
repl.itにログインして、下のスニペットを fork し、コンソールで cargo run とタイプして実行すると、上記のコードを試すことができます。
実行結果
※初回は必要なモジュールをインストールしたりでドバーっと出力が出ますが、以下ではその出力は省略しています
> cargo run
Compiling rust_mt_example v0.0.2 (/home/runner/RustReentantLockedGlovalValMap)
Finished dev [unoptimized + debuginfo] target(s) in 3.37s
Running `target/debug/rust_mt_example`
KeyValue { key: Some("ryo_grid"), value_data: "pythonista", data_id: Some(1000) }
KeyValue { key: Some("ryo_grid"), value_data: "Rustacean", data_id: Some(1000) }
KeyValue { key: Some("rust"), value_data: "before_mod", data_id: Some(1000) }
KeyValue { key: Some("rust"), value_data: "after_mod", data_id: Some(1000) }
解説
-
- VecでできたことをHashMapでどのように記述すれば良いか(冒頭の一つ目の課題)
- gval.rs を見ていただくと、GlobalDatas構造体に all_data_dict というメンバが増えています。これがHashMapを用いるために追加したメンバです
- 型宣言としては、HashMap内の key と value の型のうち、valueの方だけ Vec の場合と同様に KeyValue型をTとした時に
Arc<ReentrantMutex<RefCell<T>>>>
と宣言しており、key の方は特に何の型でラップすることもなく String と宣言しています - 格納している要素を、keyとvalueの両方を参照する形でイテレートする際などはこのような定義で問題ないのだろうか、と考えたりするのですが、少なくとも現在の型宣言だと、誰も可変参照を得ることはできない・・・はずなので文句を言われないのではないかと考えています
- なお、keyとして格納されているデータ自体を更新するような処理は、今回書き換えようとしているシミュレータには存在しないため、可変である必要はありません(Rustに限らず、そのようなコードはあまり無いとは思いますが)
- main関数では Vecの場合と同様に、一つ要素(KeyValue型)を格納し、関数でその要素を取得してみて、内容をprintlnしてから、その要素の内容を書き換えて、再度printlnするということをしています
- 同じメモリ領域に存在するKeyValue型のデータの内容が確かに書き換えられていることが分かるかと思います
-
- 関数で要素を返そうとした際の
Error : returns a value referencing data owned by the current function
をどう回避するか?(冒頭の2つ目の課題)
- 関数で要素を返そうとした際の
- きっと同じようなことをしている他のRustなコードベースを読んでみれば分かるに違いない、と考え、RustによるChord(DHT)実装を管理している以下のGitHubリポジトリのコードを読んでみました
- Benestar/rust-chord : Implementation of Chord - A Distributed Hash Table in Rust
- コードベースの規模感がちょうど良い感じだったのと、なんぼRustで書いてあるとはいえ、大体同じようなものを作っているので、やろうとしている処理の読解には苦労しないであろう、ということでこのリポジトリを選びました
- トイコードよりは難しいコードを書こうと思って苦戦したら、お作法を理解するために、読めそうな他人のコードを読んでみる、というのはプログラミング言語の学習において良い戦略だと考えています
- 結論としては、Arc型の値が得られているのであれば、その参照を用いて
Arc::clone(Arc型の参照)
とすると、Arc型の値が返ってきて、それを関数で return した場合は、上述のコンパイルエラーとなることなく値を返すことができました- マルチスレッドで共有データを扱う場合、Arc型と仲良しになることが必要そうです
- そんなわけで、main関数の上に定義されている2つの関数では、値を返す際に
Arc::clone(Arc型の参照)
とするようにしてあります- ただ、今更の話ですが、Arc型はマルチスレッドなプログラムで用いる、C++で言うところのスマートポインタのようなものですが(非マルチスレッドだと同じ用途にRc型を用いる)、同じデータに対する参照が存在する場合、同時には可変参照が1つであるか、もしくは不変参照が複数(ここでは1つの場合も含む)のいずれかでなければならないというRustの大原則があるので、その点は気を付けて用いる必要があります。とはいえ、マルチスレッドで多少複雑なコードになると苦労するかもな・・・と思ったりもしています
enjoy!