Mutex でデータの競合を防ぐ(排他制御)

Recipe ID: rust-007

前のレシピ (rust-006) では、State を使ってデータを共有しましたが、それは「読み取り専用」でした。
アプリの実行中にデータを「書き換える」(例:カウンターを増やす、リストに追加する)には、Mutex (Mutual Exclusion: 排他制御) という仕組みが必要です。

Rust では、複数の場所(スレッド)から同時にデータを書き換えるとバグの原因になるため、コンパイラが厳しくチェックします。
Mutex を使うと、「今このデータを触っているのは自分だけ」という状態(ロック)を作ることができ、安全に書き換えができるようになります。

1. 同期コマンドの場合 (std::sync::Mutex)

通常の(async ではない)コマンドで使う場合は、標準ライブラリの std::sync::Mutex を使います。

状態の定義 (State)

構造体のフィールドを Mutex<T> (Tはデータの型) で包みます。

use std::sync::Mutex;

struct CounterState {
    // i32型のデータを Mutex で保護する
    count: Mutex<i32>,
}

初期化して登録 (lib.rs)

src-tauri/src/lib.rs 内で初期化します。

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        // 初期化:Mutex::new(初期値) で包む
        .manage(CounterState { count: Mutex::new(0) })
        .invoke_handler(tauri::generate_handler![increment])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

コマンドの実装

#[tauri::command]
fn increment(state: tauri::State<'_, CounterState>) -> i32 {
    // 1. .lock() でロックを取得します。
    //    もし他の誰かがロックしていたら、解放されるまでここで待ちます。
    // 2. .unwrap() はロック取得に失敗した場合(通常は起きない)にパニックさせるために記述します。
    let mut num = state.count.lock().unwrap();
    
    // num は現在「可変参照」のような状態です。
    // *num とすることで中身の値(i32)にアクセスできます。
    *num += 1;
    
    println!("Count: {}", *num);

    // 現在の値を返します。コピーされるので安全です。
    *num
    
    // 関数エラーが終わると、`num` 変数がスコープから外れ、自動的にロックが解除されます。
}

2. 非同期コマンドの場合 (tokio::sync::Mutex)

async コマンドの中でロックを使う場合は、必ず tokio::sync::Mutex を使ってください
標準の std::sync::Mutex をロックしたまま await (待機) すると、デッドロック(処理が永久に止まる)などの深刻な問題を引き起こす可能性があります。

状態の定義と初期化

use tokio::sync::Mutex; // ライブラリが違います!

struct AsyncState {
    data: Mutex<Vec<String>>,
}

// 初期化は同様に .manage(AsyncState { data: Mutex::new(vec![]) })

コマンドの実装

#[tauri::command]
async fn add_item(state: tauri::State<'_, AsyncState>, item: String) -> Result<(), String> {
    // 非同期ロックなので .lock().await を使います
    // .unwrap() は不要です(Tokioのロックは poisoned 状態がないため)
    let mut data = state.data.lock().await;
    
    // 重い処理のシミュレーション
    // ロック(data)を持ったまま await しても、TokioのMutexなら安全です
    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    
    // データを変更
    data.push(item);
    
    println!("現在のリスト: {:?}", *data);
    
    Ok(()) // 自動的にロック解除
}

まとめ

  • データを変更したいなら: Mutex で包む。
  • 通常のコマンドなら: std::sync::Mutex を使い、.lock().unwrap() で開く。
  • async コマンドなら: tokio::sync::Mutex を使い、.lock().await で開く。