Go言語と Rust で Mutex による排他制御

以下の 3通りを Go 言語と Rust でそれぞれ実装してみました。

サンプルコードは http://github.com/fits/try_samples/tree/master/blog/20201122/

Go 言語の場合

まずは Go 言語による実装です。 Go 言語では goroutine で軽量スレッドによる並行処理を実施できます。

ここでは、goroutine の終了を待機するために sync.WaitGroup を使いました。

WaitGroup では Add した数に相当する Done が呼び出されるまで Wait でブロックして待機する事が可能です。

go_mutex_sample.go
package main

import (
    "fmt"
    "sync"
)

type Data struct {
    value int
}

// (a)
func noLock() {
    var wg sync.WaitGroup

    var ds []Data

    for i := 0; i < 100; i++ {
        wg.Add(1)

        go func() {
            ds = append(ds, Data{i})
            wg.Done()
        }()
    }
    // goroutine の終了を待機
    wg.Wait()

    fmt.Println("(a) noLock length =", len(ds))
}

// (b)
func useMutex() {
    var wg sync.WaitGroup
    var mu sync.Mutex

    var ds []Data

    for i := 0; i < 100; i++ {
        wg.Add(1)

        go func() {
            mu.Lock()
            ds = append(ds, Data{i})
            mu.Unlock()

            wg.Done()
        }()
    }

    wg.Wait()

    fmt.Println("(b) useMutex length =", len(ds))
}

// (c)
func useRWMutex() {
    var wg sync.WaitGroup
    var mu sync.RWMutex

    var ds []Data

    for i := 0; i < 100; i++ {
        wg.Add(1)

        go func() {
            mu.Lock()
            ds = append(ds, Data{i})
            mu.Unlock()

            wg.Done()
        }()
    }

    for i := 0; i < 5; i++ {
        wg.Add(1)

        go func() {
            mu.RLock()
            fmt.Println("(c) progress length =", len(ds))
            mu.RUnlock()

            wg.Done()
        }()
    }

    wg.Wait()

    fmt.Println("(c) useRWMutex length =", len(ds))
}

func main() {
    noLock()

    println("-----")

    useMutex()

    println("-----")

    useRWMutex()
}

実行結果は以下の通りです。

排他制御を行っていない (a) では、同じ状態の ds に対して複数の goroutine が ds = append(ds, Data{i}) を実行してしまうケースが発生するため、基本的に結果が 100 にはなりません。

実行結果
> go build go_mutex_sample.go

> go_mutex_sample
(a) noLock length = 95
-----
(b) useMutex length = 100
-----
(c) progress length = 71
(c) progress length = 72
(c) progress length = 72
(c) progress length = 72
(c) progress length = 72
(c) useRWMutex length = 100

Rust の場合

次は Rust による実装です。 ここでは thread::spawn で並行処理を実施し join で待機しています。

基本的に Rust では、Go 言語で実装した noLock のようなスレッドセーフでは無い処理はコンパイルエラーとなって実装できないように工夫されています。

スレッド間で安全に所有権を共有するには、スレッドセーフな参照カウントのポインタである Arc を使用する事になります。

排他制御なしの (a) の処理(下記コードの no_lock)をコンパイルが通るように一応は実装してみましたが、この Arc::get_mut(&mut ds) は常に None を返すので ds.push(Data(i)) の処理は実行されません。

rust_mutex_sample.rs
use std::thread;
use std::sync::{Arc, Mutex, RwLock};

struct Data(i32);

// (a)
fn no_lock() {
    let mut hs = Vec::new();
    let ds = Arc::new(Vec::new());

    for i in 0..100 {
        let mut ds = ds.clone();

        hs.push(
            thread::spawn(move || {
                if let Some(ds) = Arc::get_mut(&mut ds) {
                    // 上記は常に None となるので下記処理は実行されない
                    ds.push(Data(i))
                }
            })
        );
    }

    for h in hs {
        let _ = h.join();
    }

    println!("(a) no_lock length = {}", ds.len());
}

// (b)
fn use_mutex() {
    let mut hs = Vec::new();
    let ds = Arc::new(Mutex::new(Vec::new()));

    for i in 0..100 {
        let ds = ds.clone();

        hs.push(
            thread::spawn(move || {
                if let Ok(mut ds) = ds.lock() {
                    ds.push(Data(i));
                }
            })
        );
    }

    for h in hs {
        let _ = h.join();
    }

    println!("(b) use_mutex length = {}", ds.lock().unwrap().len());
}

// (c)
fn use_rwlock() {
    let mut hs = Vec::new();
    let ds = Arc::new(RwLock::new(Vec::new()));

    for i in 0..100 {
        let ds = ds.clone();

        hs.push(
            thread::spawn(move || {
                if let Ok(mut ds) = ds.write() {
                    ds.push(Data(i));
                }
            })
        );
    }

    for _ in 0..5 {
        let ds = ds.clone();

        hs.push(
            thread::spawn(move || {
                if let Ok(ds) = ds.read() {
                    println!("(c) progress length = {}", ds.len());
                }
            })
        );
    }

    for h in hs {
        let _ = h.join();
    }

    println!("(c) use_rwlock length = {}", ds.read().unwrap().len());
}

fn main() {
    no_lock();

    println!("-----");

    use_mutex();

    println!("-----");

    use_rwlock();
}

実行結果は以下の通りです。

no_lock 内の ds.push(Data(i)) は実行されないので結果は 0 となります。

実行結果
> rustc rust_mutex_sample.rs

> rust_mutex_sample
(a) no_lock length = 0
-----
(b) use_mutex length = 100
-----
(c) progress length = 99
(c) progress length = 99
(c) progress length = 99
(c) progress length = 99
(c) progress length = 99
(c) use_rwlock length = 100