Goがあらゆる点において並行処理で優れているわけではないよ、という話。
- Goは組み込みチャネルやgoroutineのような仕組みもあり実際に並列並行処理に強みがあるが落とし穴はそれなりにある
- WaitGroupのカウントアップをgoroutine内でやるな、とか
- ここでは共有資源へのアクセス・書き込みの安全性について考える
- Goは共有資源への操作にかんする型レベルでの安全性にかんして欠如している
- atomicIntなどatomic操作の型が提供されているが、複数のスレッドから参照されるリソースへの型チェックは機能しない
- 共有リソースへの正しい操作はほとんどユーザの責任となる
- 基本的にチャネルは値のみ・Mutexで囲んで使うなどイディオムでの対処となっている
package main
import (
"fmt"
"sync"
)
type Money struct {
price int32 // ここをatomicInt32にするなどを忘れてしまう
}
// 実際にこういった問題が顕在するのはグローバル変数に対する
// 書き込み・読み出しで、イディオムとしてはMutex(orアトミック操作)
// を用いた排他制御を使うべきという結論になるが、ここで比較として
// 示したいのは型による保護機構が欠如しているためコンパイル時に
// Mutex or アトミック操作忘れに気がつくことができないという点。
func main() {
wg := sync.WaitGroup{}
mine := &Money{price: 0}
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
// sync/atomic を用いるべきだが、これを静的に検証することはできない
mine.price += 1
wg.Done()
}()
}
wg.Wait()
fmt.Println(mine)
}
Goは競合状態(が起こりうるコード)を検査するための静的な仕組みをもっていないかわりに、race detectorという実行時に検査を行うツールを提供している。
$ go run -race race.go
==================
WARNING: DATA RACE
Read at 0x00c00013601c by goroutine 8:
main.main.func1()
/home/kubo39/dev/kubo39/race2.go:24 +0x3b
Previous write at 0x00c00013601c by goroutine 7:
main.main.func1()
/home/kubo39/dev/kubo39/race2.go:24 +0x4b
Goroutine 8 (running) created at:
main.main()
/home/kubo39/dev/kubo39/race2.go:22 +0x84
Goroutine 7 (finished) created at:
main.main()
/home/kubo39/dev/kubo39/race2.go:22 +0x84
==================
&{100}
Found 1 data race(s)
exit status 66
- D言語は共有リソースに対するアクセスの問題に対して型によって操作を限定している
- immutable型: そもそも読み取りしかできないので安全な操作が保証
- shared型: 共有資源に対する操作をアトミックな操作に限定
- 上記よりspawnで生成したスレッドからの共有資源への操作はアトミックな操作に限定できる
- ただし生のスレッドAPIでは限定できないので自己責任
- 常にstd.concurrencyを使うことを推奨
- そうでない場合自分でsharedを受け取るようなAPIを作ってやる必要がある
import core.atomic;
import std.concurrency;
import std.stdio;
struct Money { int price; }
// 本来はshared型はグローバル変数の共有で用いられる。
// sharedはグローバル変数以外にもTLSでも用いることができる。
// shared型は必ずアトミック操作で行われなければならないため
// 非アトミックな演算を誤って行おうとした場合にコンパイル時に
// 気がつくことが可能である。
// またspawnで生成したスレッドにはimmutableな値かshared型の値
// しか渡すことができないため複数のリソースへの書き込みは
// 必ずアトミック操作になることが保証できる。
//
void main()
{
auto tid = spawn((){
auto src = receiveOnly!(shared(Money)*);
// src.price++;
// > コンパイルエラー!
// > read-modify-write operaions are not allowed for `shared` variables!
// shared型へのread-modify-write操作はアトミック操作に限定される。
src.price.atomicOp!"+="(1);
ownerTid.send(true);
});
// auto mine = new Money(1);
// > コンパイルエラー!
// > Aliases to thread-local data must be shared!
// spawnしたスレッドに渡せるのはimmutableかshared型だけ。
shared(Money)* mine = new shared Money(1);
tid.send(mine);
// mine.price++;
// > コンパイルエラー!
// > read-modify-write operaions are not allowed for `shared` variables!
// shared型へのread-modify-write操作はアトミック操作に限定される。
mine.price.atomicOp!"+="(1);
auto ret = receiveOnly!bool;
assert(ret);
writeln(*mine);
}
- RustもOwnership・Send/Syncのような型クラスで共有資源へのアクセスを限定
- グローバル変数はMutexで必ず囲む必要があるなどより強力な制約がある
- move semanticsがあるため(danglingが起きない)プリミティブな操作はより強力
- 組み込みでSend/Syncな型の例: Arc/Mutex
- Send: 別スレッドに渡せる
- Sync: 共有資源に複数からアクセスされる
- unsafe impl Sync for XXX などで騙せるがそれはユーザの責任
- Syncは(プリミティブな意味での)アトミック操作のみしかできないわけではない
- Rustの型システムはshared XOR mutableの原則があるので共有参照の書き込みが型レベルで排他される
- 安全性と効率性の両面で優れている
- そのぶん難しい概念ではある
また、たとえばRustの制約としてmutableな借用が起きているあいだはimmutableな参照をとることができないというものがある。
fn main() {
let mut a = 42;
let b = &mut a; // 可変参照
let c = &a; // 不変参照
*b += 1; // もしこれが可能だと
println!("{}", c); // cの結果が暗黙に変わってしまう
}
以下の例は可変参照を他スレッドに渡したあとに不変参照をとれないというコンパイルエラー。
use std::thread;
#[derive(Debug)]
struct Money {
price: i32,
}
fn main() {
let mut mine = Money { price: 0 };
thread::spawn(|| {
mine.price += 1;
});
println!("{:?}", mine);
}
Rustのエラーメッセージは親切なので、moveによる所有権の移譲についてのヒントも出してくれる。
$ rustc race.rs
error[E0373]: closure may outlive the current function, but it borrows `mine`, which is owned by the current function
--> race.rs:10:19
|
10 | thread::spawn(|| {
| ^^ may outlive borrowed value `mine`
11 | mine.price += 1;
| ---------- `mine` is borrowed here
|
note: function requires argument type to outlive `'static`
--> race.rs:10:5
|
10 | / thread::spawn(|| {
11 | | mine.price += 1;
12 | | });
| |______^
help: to force the closure to take ownership of `mine` (and any other referenced variables), use the `move` keyword
|
10 | thread::spawn(move || {
| ++++
error[E0502]: cannot borrow `mine` as immutable because it is also borrowed as mutable
--> race.rs:13:22
|
10 | thread::spawn(|| {
| - -- mutable borrow occurs here
| _____|
| |
11 | | mine.price += 1;
| | ---------- first borrow occurs due to use of `mine` in closure
12 | | });
| |______- argument requires that `mine` is borrowed for `'static`
13 | println!("{:?}", mine);
| ^^^^ immutable borrow occurs here
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to 2 previous errors
Some errors have detailed explanations: E0373, E0502.
For more information about an error, try `rustc --explain E0373`.