🦀

Mutex と RwLock の違い

2024/04/28に公開

Rust で並行プログラミング時の MutexRwLock の違いについて説明します。
まずはじめに、並行プログラミング時のロックという概念について説明します。

ロックとは?

ロックとは、共有データへのアクセスを制限する仕組みのことです。共有データとは、複数のスレッドから同時にアクセスされる可能性のあるデータのことを指します。
ロックを取得しているスレッドは、そのロックを解放するまで共有リソースへの排他的なアクセス権を持ちます。他のスレッドはその間、そのロックを取得することができません。

並行プログラミングにおける「ロック」は、大きく分けて以下の2種類になります。

  1. 読み取りロック
    • 共有データを読み取るためのロック
  2. 書き込みロック
    • 共有データを書き換えるためのロック

Mutexとは?

Mutexは、ロックを取得したスレッドだけが共有リソースへの書き込み/読み取りアクセスを独占できます。他のスレッドはロックが解放されるまでアクセスできません。

Mutexの図をコード化したもの
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let shared_data = Arc::new(Mutex::new(0));

    let shared_data_clone1 = Arc::clone(&shared_data);
    let thread1 = thread::spawn(move || {
        let data = shared_data_clone1.lock().unwrap();
        println!("スレッド1: データ読み取り - {}", data);
    });

    let shared_data_clone2 = Arc::clone(&shared_data);
    let thread2 = thread::spawn(move || {
        let mut data = shared_data_clone2.lock().unwrap();
        *data += 2; // データ書き込み
        println!("スレッド2: データ書き込み - {}", data);
    });

    let shared_data_clone3 = Arc::clone(&shared_data);
    let thread3 = thread::spawn(move || {
        let data = shared_data_clone3.lock().unwrap();
        println!("スレッド3: データ読み取り - {}", data);
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
    thread3.join().unwrap();

    let final_data = shared_data.lock().unwrap();
    println!("最終データ: {}", *final_data);
}

このコードでは、以下のようになっています。

  1. Arc<Mutex<i32>>を使って共有データを初期化します。
  2. 3つのスレッドを作成し、それぞれが共有データへのアクセスを試みます。
    • スレッド1は読み取りアクセスを行います。
    • スレッド2は書き込みアクセスを行い、データを2加算します。
    • スレッド3は読み取りアクセスを行います。
  3. 各スレッドが終了するのを待機します。
  4. 最終的な共有データの値を出力します。

共有データへのアクセスについて

  • lock()メソッドは排他的なロックを取得します。
  • つまり、ある1つのスレッドがlock()を呼び出してロックを取得している間は、他のスレッドはlock()を呼び出してもロックを取得できず、待機状態になります。
  • ロックを取得したスレッドは、共有データへの読み取り/書き込みの両方ができます。

実行結果は以下のようになります。

スレッド1: データ読み取り - 0
スレッド2: データ書き込み - 2
スレッド3: データ読み取り - 2
最終データ: 2

RwLockとは?

RwLockは、書き込みロックを取得したスレッドだけが共有リソースへの書き込みアクセスを独占できます。読み取りロックの場合は複数のスレッドが同時に取得可能ですが、書き込みロックが取得されている間は、他のスレッドの読み取り/書き込みアクセスが排除されます。

RwLockの図をコード化したもの
use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let shared_data = Arc::new(RwLock::new(0));

    let shared_data_clone1 = Arc::clone(&shared_data);
    let thread1 = thread::spawn(move || {
        let data = shared_data_clone1.read().unwrap();
        println!("スレッド1: データ読み取り - {}", data);
    });

    let shared_data_clone2 = Arc::clone(&shared_data);
    let thread2 = thread::spawn(move || {
        let data = shared_data_clone2.read().unwrap();
        println!("スレッド2: データ読み取り - {}", data);
    });

    let shared_data_clone3 = Arc::clone(&shared_data);
    let thread3 = thread::spawn(move || {
        let mut data = shared_data_clone3.write().unwrap();
        *data += 1; // データ書き込み
        println!("スレッド3: データ書き込み - {}", data);
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
    thread3.join().unwrap();

    let final_data = shared_data.read().unwrap();
    println!("最終データ: {}", *final_data);
}

このコードでは、以下のようになっています。

  1. Arc<RwLock<i32>>を使って共有データを初期化します。
  2. 3つのスレッドを作成し、それぞれが共有データへのアクセスを試みます。
    • スレッド1は読み取りアクセスを行います。
    • スレッド2も読み取りアクセスを行います。
    • スレッド3は書き込みアクセスを行い、データを1加算します。
  3. 各スレッドが終了するのを待機します。
  4. 最終的な共有データの値を出力します。

共有データへのアクセスについて

  • read()メソッドは複数のスレッドが同時に呼び出せる読み取り用のロックを取得します。
  • write()メソッドは排他的な書き込み用のロックを取得します。
  • 複数のスレッドが同時にread()を呼び出してデータの読み取りロックを取得することができます。
  • しかし、write()を呼び出したスレッドが書き込みロックを取得している間は、他のスレッドは読み取りロックも書き込みロックも取得できなくなります。 つまり、そのスレッド以外はデータにアクセスできなくなります。

実行結果は以下のようになります。

スレッド1: データ読み取り - 0
スレッド2: データ読み取り - 0
スレッド3: データ書き込み - 1
最終データ: 1

MutexRwLock の違い

  • Mutexは読み取り/書き込みともに一度に1つのスレッドのみがアクセス可能な排他ロックです。
  • RwLockは読み取りの場合は複数のスレッドが同時にアクセス可能ですが、書き込みの場合は一度に1つのスレッドのみがアクセス可能です。
ロック種類 Mutex RwLock
読み取り 一度に1つのスレッドのみ 複数のスレッドが同時に可能
書き込み 一度に1つのスレッドのみ 一度に1つのスレッドのみ

MutexRwLockの違いは、読み取りロックの取り扱い方法が異なる点にあります。

これらの違いにより、以下のようなメリット/デメリットがあります。

Mutexのメリット:

  • 実装が単純で、ロックの種類を気にする必要がありません。

Mutexのデメリット:

  • 読み取りの場合でもロックが排他的になるため、並行性が低下します。

RwLockのメリット:

  • 読み取りが多い場合、並行性が高まり、パフォーマンスが向上します。

RwLockのデメリット:

  • ロックの種類(読み取り/書き込み)を意識する必要があり、実装が複雑になります。

まとめ

Mutex

  • 排他的ロック
  • 一度に1つのスレッドのみがロックを取得可能
  • 読み取り/書き込みの区別なく、ロックを取得したスレッドのみがデータにアクセス可能
  • 実装が単純
  • データの整合性は保たれるが、並行性が低い

RwLock

  • 読み取り用ロックと書き込み用ロックを別々に提供
  • 複数のスレッドが同時に読み取りロックを取得可能
  • 書き込みロックは一度に1つのスレッドのみが取得可能
  • 書き込みロックが取得されている間は、他のスレッドは読み取り/書き込みともにロックを取得できない
  • 実装が複雑
  • 読み取りの並行性が高いので、読み取りが主体の場合はパフォーマンスが向上する

使い分け

  • データを頻繁に書き換える場合は、Mutexを使う
  • データの読み取りが主体で、書き換えが少ない場合は、RwLockを使う
  • 単純な実装を優先する場合は、Mutexを使う
  • 読み取りの並行性を重視する場合は、RwLockを使う

Discussion