---
title: "Goのsync.MutexとRWMutexの違いと使い分け｜実測ベンチで選ぶ排他制御"
url: "https://www.issoh.co.jp/tech/details/5549/"
published: 2025-02-26
updated: 2026-09-23
categories: ["Go"]
publisher: "株式会社一創"
---

# Goのsync.MutexとRWMutexの違いと使い分け｜実測ベンチで選ぶ排他制御

Goで共有データを複数のゴルーチンから触るとき、`sync.Mutex` と `sync.RWMutex` のどちらを選ぶかは「読み取りが多いならRWMutex」という一文で片付けられがちです。しかし手元のgo1.26.5で測ると、読み取り100%でも並列度2までは`Mutex`のほうが速く、書き込みが1%混ざっただけで並列度8以上では`RWMutex`が`Mutex`に負けました。この記事では両者の違いを構造から押さえたうえで、実測値と公式ドキュメントの記述をもとに、どちらを選ぶかを条件で判断できる形にまとめます。

## まとめ：MutexとRWMutexの選び方

結論を先に置きます。判断の軸は「読み取りが多いかどうか」ではなく、**並列度と書き込み比率の組み合わせ**です。

- 迷ったら `sync.Mutex`。8バイト（RWMutexは24バイト）で、メソッドが`Lock`／`Unlock`の2つだけなので取り違えようがない。今回の測定では書き込み比率0〜10%で性能も横ばい
- `sync.RWMutex` が読み取り専用ベンチで優位だったのは`GOMAXPROCS`が4以上のときで、2までは`Mutex`のほうが速い（後述の表B）
- 1,024キーのマップに書き込みを1%混ぜると、`GOMAXPROCS`\=8以上で`RWMutex`が`Mutex`に負ける。ライター優先設計のため、待機中のライターが後続のリーダーを止めるから（表C）
- 数値はgo1.26.5・Intel Core i9-9880Hでの実測。ロック内の処理量や実行環境が変われば逆転点も動く
- キーごとに独立した読み書きをするマップなら、ロックを比べる前に `sync.Map` とシャーディングを検討する
- `RWMutex`には再帰的な`RLock`がデッドロックするという固有の罠がある

## sync.MutexとRWMutexの違い：メソッドと同時実行の可否

両者はどちらも`sync`パッケージの排他ロックですが、公開しているメソッドと、同時に何本のゴルーチンが入れるかが違います。

| 項目        | sync.Mutex                               | sync.RWMutex                      |
| --------- | ---------------------------------------- | --------------------------------- |
| 取得メソッド    | Lock / TryLock                           | Lock / TryLock / RLock / TryRLock |
| 解放メソッド    | Unlock                                   | Unlock / RUnlock                  |
| 同時に入れる数   | 常に1本                                     | リーダーは複数、ライターは1本                   |
| 構造体サイズ    | 8バイト                                     | 24バイト                             |
| 内部の状態管理定数 | mutexLocked / mutexWoken / mutexStarving | rwmutexMaxReaders = 1<<30         |
| ゼロ値       | そのまま使える                                  | そのまま使える                           |
| 再帰取得      | 不可（自己デッドロック）                             | RLockも不可                          |

サイズはgo1.26.5のdarwin/amd64で`unsafe.Sizeof`を取った実測値です。`TryLock`・`TryRLock`はGo 1.18で追加されたもので、それ以前のコードには存在しません。

### ミューテックスの意味とGoにおける排他制御

ミューテックスはmutual exclusion（相互排他）の略で、Goの標準ライブラリでも`sync.Mutex`のドキュメント冒頭は「A Mutex is a mutual exclusion lock.」と定義しています。守るのはコードではなくデータです。同じ変数を触る箇所が3か所あるなら、その3か所すべてが同じ`Mutex`を取ってからアクセスしていなければ意味がありません。

OSやC++の文脈で言うミューテックスはスレッドを止めますが、Goの`sync.Mutex`が止めるのはゴルーチンです。待たされたゴルーチンはランタイムのセマフォで停止（park）し、OSスレッドは別のゴルーチンを走らせに行きます。この切り替えを担うのがGMPスケジューラで、仕組みは[GoroutineのM:Nスケジューリングの解説](/tech/details/4542/)にまとめています。

なお`sync`パッケージが公開している型は`Cond`・`Locker`・`Map`・`Mutex`・`Once`・`Pool`・`RWMutex`・`WaitGroup`の8つで、セマフォ型はありません。同時実行数をNに制限したい場合は、バッファ付きチャネルを使うか、`golang.org/x/sync/semaphore`（2026年9月23日時点でv0.23.0、公開は2026年8月31日）の`Weighted`を使います。

### RWMutexの内部構造：Mutexとリーダーカウンタの組み合わせ

go1.26.5の`src/sync/rwmutex.go`を開くと、`RWMutex`は次の5フィールドで構成されています。

```
type RWMutex struct {
	w           Mutex        // ライター同士の競合を解決する内部Mutex
	writerSem   uint32       // ライターがリーダーの完走を待つセマフォ
	readerSem   uint32       // リーダーがライターの完了を待つセマフォ
	readerCount atomic.Int32 // RLock中のリーダー数（ライター待機中は負の値になる）
	readerWait  atomic.Int32 // 退出待ちのリーダー数
}

const rwmutexMaxReaders = 1 << 30
```

先頭に`Mutex`が埋まっている点に注目してください。`RWMutex.Lock`はまず内部の`w.Lock()`でライター同士の順番を決め、そのうえで`readerCount`から`rwmutexMaxReaders`を引いて値を負に振り、「ライターが待っている」ことをリーダー側へ知らせます。`RWMutex`は`Mutex`の上位互換ではなく、`Mutex`に読み取り用の会計処理を足したものだと考えると挙動が読めます。

リーダー側の高速パスは`readerCount.Add(1)`の1回だけです。裏を返すと、複数のコアが同時に`RLock`を呼ぶと全員が同じ1本のカウンタに書き込むことになり、そのキャッシュラインをコア間で奪い合います。この性質が実測値にどう出るかは次章の表で確認します。

[sync.RWMutexの公式リファレンス](https://pkg.go.dev/sync#RWMutex)が明記している制約も押さえておきます。「If any goroutine calls RWMutex.Lock while the lock is already held by one or more readers, concurrent calls to RWMutex.RLock will block until the writer has acquired (and released) the lock」。つまりライターが1本待ち始めた時点で、後続のリーダーは全員止まります。後続のリーダーを止めることで、待機中のライターに取得の順番を回す設計です。ライターの餓死は防げますが、性能面ではこれが効いてきます。

## MutexとRWMutexの実測比較：並列度と書き込み比率

ここからはgo1.26.5・Intel Core i9-9880H（8物理コア／16論理CPU、darwin/amd64）で実際に測った値を出します。「RWMutexのほうが軽い」と結論づけるベンチマークをよく見かけますが、単一ゴルーチンで測れるのはロック1回あたりのコストだけで、`RWMutex`の狙いである並行読み取りの効果はそこに出ません。並列度と書き込み比率を振ってはじめて、どちらを選ぶべきかが決まります。

以下の4表はいずれも`go test -run XXX -bench ... -benchtime 1s`で測り、`-count`で繰り返した各回の値から中央値（偶数回のときは中央2値の平均）を採っています。クリティカルセクションの中身は、表Aがロックの取得と解放のみ、表Bが固定キー1件のマップ参照、表Cが1,024キーのマップに対する1件の参照または代入です。

### 非競合時のMutex・RWMutexの取得と解放のコスト

まず競合が一切ない単一ゴルーチンで、ロックの取得と解放だけを繰り返した結果です。`-benchtime=1s -cpu=1 -count=5`の5回計測で、最小値と最大値、中央値を並べます。

| 操作                    | 最小       | 中央値      | 最大       |
| --------------------- | -------- | -------- | -------- |
| Mutex Lock+Unlock     | 35.82 ns | 36.62 ns | 37.20 ns |
| RWMutex RLock+RUnlock | 31.28 ns | 31.55 ns | 32.47 ns |
| RWMutex Lock+Unlock   | 60.66 ns | 62.83 ns | 64.87 ns |

`RLock`そのものは重くありません。5回とも`Mutex.Lock`を下回り、中央値で約1.16倍速い。高速パスが`readerCount.Add(1)`の1回で済むぶん、ロックの獲得・解放を伴う`Mutex`より短いからです。重いのは`RWMutex`の書き込みロックのほうで、`Mutex`の約1.7倍かかります。内部`Mutex`の取得にリーダーカウンタの操作が乗るためです。つまり`RWMutex`を選ぶコストは書き込み側に集中して現れます。

### 読み取り専用ベンチにおける並列度別の測定結果

次に`b.RunParallel`で並列度を変えながら、読み取りだけを回した結果です。表の単位はns/opで、ベンチ全体の経過時間を総操作数で割った値であり、個々の呼び出しの待ち時間ではありません。`-count=4`の中央値を載せます。

| GOMAXPROCS | Mutex    | RWMutex | 勝者             |
| ---------- | -------- | ------- | -------------- |
| 1          | 55.7 ns  | 56.9 ns | ほぼ同着           |
| 2          | 68.3 ns  | 77.0 ns | Mutex（1.13倍）   |
| 4          | 94.8 ns  | 75.5 ns | RWMutex（1.26倍） |
| 8          | 160.5 ns | 73.7 ns | RWMutex（2.18倍） |
| 16         | 177.4 ns | 70.4 ns | RWMutex（2.52倍） |

逆転は並列度2と4のあいだで起きています。並列度2では`Mutex`のほうが速く、4を超えると差が開いて16では`RWMutex`が2.5倍速い。注目したいのは右の列の形です。`Mutex`のns/opは今回の測定では並列度とともに増える（55.7→177.4 ns）のに対し、`RWMutex`は並列度2以降ほぼ横ばい（70〜77 ns）で、コア数に比例して速くなるわけでもありません。リーダー全員が`readerCount`という同じ1本のカウンタを更新するため、コア間でそのキャッシュラインを奪い合うからです。`RWMutex`の利点は「複数の読み取り処理を同時に実行できる」ことだと捉えると、次の表の結果も読めます。

### GOMAXPROCS=16での書き込み比率別の測定結果

実際のコードで書き込みが完全にゼロということはまずありません。1,024キーのマップに対して読み書きを混ぜ、並列度16で測りました。クリティカルセクションはマップ1回の参照または代入で、`-count=4`の中央値です。

| 書き込み比率 | Mutex    | RWMutex  | sync.Map |
| ------ | -------- | -------- | -------- |
| 0%     | 198.5 ns | 71.9 ns  | 7.95 ns  |
| 1%     | 204.1 ns | 220.2 ns | 10.38 ns |
| 10%    | 214.1 ns | 317.2 ns | 21.17 ns |

0%では`RWMutex`が2.8倍速いのに、書き込みが1%入った時点で逆転され、10%では1.5倍遅くなります。`Mutex`のほうは書き込み比率が0%から10%まで変わっても198.5→214.1 nsとほぼ横ばいです。実装を読むと、前節で引用したライター優先の仕様で説明が付きます。書き込みロックが待機を始めた後に到着したリーダーはブロックされ、ライターが抜けるたびに`readerSem`経由で起こし直されるからです。

ただしこの逆転は並列度にも依存します。書き込み比率を1%に固定したまま`GOMAXPROCS`を振り直すと、次のようになりました（`-count=3`の中央値）。

| GOMAXPROCS（書き込み1%） | Mutex    | RWMutex  | 勝者    |
| ------------------ | -------- | -------- | ----- |
| 2                  | 90.09 ns | 91.98 ns | ほぼ同着  |
| 4                  | 126.8 ns | 123.7 ns | ほぼ同着  |
| 8                  | 184.3 ns | 200.3 ns | Mutex |
| 16                 | 203.1 ns | 247.4 ns | Mutex |

並列度4まではほぼ互角で、差が開くのは8以上です。読み取り100%の表Bでは並列度4で`RWMutex`が勝っていたことと合わせると、この処理量での`RWMutex`の優位は「並列度が高い」と「書き込みがほぼゼロ」が揃ったときに出ており、どちらかが崩れると`Mutex`と並ぶか負けました。ロック内の処理が長ければ並行読み取りの取り分は増えるので、判断は自分のワークロードで測り直してください。

書き込みが混ざる前提なら、まず`Mutex`で書き、プロファイルでロック競合が実際にボトルネックだと分かってから`RWMutex`を検討する順序が妥当です。なお、安全に公開したあと一切書き換えない設定テーブルなら、そもそもロック自体が要りません。[公式FAQ](https://go.dev/doc/faq#atomic%5Fmaps)は「Map access is unsafe only when updates are occurring.」「it is safe for them to access the map concurrently without synchronization」と明記しています。`RWMutex`を入れる前に、その共有データが本当に更新されるのかを確かめてください。

同じ表の`sync.Map`列が桁違いに速いのは、このベンチが「1,024個のばらばらなキーを読み書きする」という`sync.Map`の得意な条件そのものだからです。Go 1.24で実装が`HashTrieMap`に置き換わり、リリースノートは「The implementation of sync.Map has been changed, improving performance, particularly for map modifications.」「For instance, modifications of disjoint sets of keys are much less likely to contend on larger maps」と説明しています。ただし、このベンチでは各ゴルーチンが同じキー列を走査しており、ゴルーチンごとに互いに素なキー集合を割り当てた条件ではありません。少数のキーに集中する用途は今回測っていないので、この倍率をそのまま当てにはできません。`sync.Map`自身のドキュメントが「Most code should use a plain Go map instead」と書いている点も含めて、万能の置き換え先ではありません。

## 共有mapを守るRWMutexの実装例

`Mutex`も`RWMutex`もゼロ値でそのまま使えます。`new`や初期化関数は不要で、守りたいデータと同じ構造体に値で埋め込むのが定石です。

```
type Store struct {
	mu sync.RWMutex
	m  map[string]int
}

func (s *Store) Get(k string) (int, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	v, ok := s.m[k]
	return v, ok
}

func (s *Store) Set(k string, v int) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.m == nil {
		s.m = make(map[string]int)
	}
	s.m[k] = v
}
```

`defer`で解放するのは、途中の`return`やpanicでもロックを取りこぼさないためです。ただし`defer`はクリティカルセクションを関数の末尾まで引き延ばすので、ロックの外でやれる重い処理（I/O、ログ出力、シリアライズ）は関数を分けてロックの外へ出します。

`Set`の中で`s.m`を確認しているのは、`var s Store`のゼロ値から使えるようにするためです。これを省くと`panic: assignment to entry in nil map`で落ちます。nilのmapは参照だけなら安全で（上の`Get`はその性質に乗っています）、panicになるのは代入と削除です。`Mutex`のゼロ値はそのまま使えるのにmapのゼロ値は書き込めない、という非対称がここに出ます。上のコードはgo1.26.5で`go run -race`まで通しています。

待ちたくない場合は`TryLock`系を使います。go1.26.5で実測した戻り値は次のとおりです。

| ロックの状態            | TryRLock | TryLock |
| ----------------- | -------- | ------- |
| ライターが保持中          | false    | false   |
| リーダーが保持中・ライター待機なし | true     | false   |
| リーダーが保持中・ライター待機あり | false    | false   |

3行目が見落としやすいところです。リーダーが入れているかどうかは、ライターが待ち行列にいるかで変わります。後述のデッドロック例で`for rw.TryRLock()`をライター待機の検出に使えるのは、この挙動があるからです。ただし標準ライブラリ自身が「use of TryLock is often a sign of a deeper problem」と注意しているとおり、常用するものではありません。

## MutexとRWMutexの誤用で起きる停止とデータ競合

### 再帰的なRLockとライター待機によるデッドロック

`RWMutex`固有の罠がこれです。`RLock`を取ったまま、同じロックの`RLock`をもう一度取る関数を呼ぶと、そのあいだにライターが割り込んだ瞬間にデッドロックします。ドキュメントも「Note that this prohibits recursive read-locking.」と明記しています。

go1.26.5で実際に止めたコードが次です。`outer`が`RLock`を保持したまま`inner`を呼び、`inner`が同じロックを再び`RLock`します。ライターは最初の`RLock`を取った後に起動し、`TryRLock`が`false`を返す（＝ライターが待ち始めた合図）まで待ってから再取得しているので、実行順によらず毎回止まります。

```
var rw sync.RWMutex

func outer() {
	rw.RLock()
	defer rw.RUnlock()
	go func() { rw.Lock(); rw.Unlock() }() // RLock保持後にライターを起動する
	for rw.TryRLock() {                    // ライターが待ち行列に入るまで回す
		rw.RUnlock()
	}
	inner()
}

func inner() {
	rw.RLock() // outerのRLockを持ったまま再取得＝ここで自分が止まる
	defer rw.RUnlock()
}

func main() {
	outer()
}
```

実行すると`fatal error: all goroutines are asleep - deadlock!` で落ち、スタックトレースには`goroutine 1 [sync.RWMutex.RLock]`と`sync.runtime_SemacquireRWMutexR`が並びます。`outer`の`defer rw.RUnlock()`は`inner`が返らない限り走らないので、自分の解放を自分で待つ形になって永久に抜けられません。ライターが待っていなければ2回目の`RLock`は通ってしまうため、テストでは再現せず本番の負荷でだけ止まる、という出方をします。ロックを取った状態で呼ぶ関数が内部で同じロックを触っていないか、リファクタリングのたびに確認が要ります。

同じ理由で`RLock`を`Lock`へ昇格させることもできません。ドキュメントの「A RWMutex.RLock cannot be upgraded into a RWMutex.Lock, nor can a RWMutex.Lock be downgraded into a RWMutex.RLock.」がそれです。

### Mutex.Lockの待機とcontextキャンセルの非連動

ロック待ちにタイムアウトを設けたい場面で`context.WithTimeout`を持ち出す解説を見かけますが、`sync.Mutex.Lock`は`context`を引数に取らず、参照もしません。実際に、200ミリ秒でタイムアウトする`context`を用意したうえで保持中の`Mutex`に`Lock`を掛けると、`context`が期限切れになってもゴルーチンは待ち続け、500ミリ秒後に保持側が`Unlock`した時点で初めて取得しました。`context`のキャンセルはロック待ちには一切効きません。

本当に待ちを打ち切りたいなら、`TryLock`とリトライを組むか、ロックではなくバッファ1のチャネルを排他に使って`select`で`ctx.Done()`と競わせます。`context`そのものの挙動やキャンセル理由の切り分けは[Goのcontext canceledとdeadline exceededの切り分け](/tech/details/3757/)で扱っています。

### 未取得ロックのUnlockによるfatal終了とrecoverの限界

ロックしていない`Mutex`を`Unlock`すると、`fatal error: sync: unlock of unlocked mutex` でプロセスが落ちます。これは`panic`ではなく`fatal`なので、`defer`と`recover`を仕掛けていても拾えません。実際に`recover`付きのテストで試すと、`recover`の行には到達せずテストバイナリごと終了します。

`RWMutex`にも同じ扱いの`fatal("sync: RUnlock of unlocked RWMutex")`と`fatal("sync: Unlock of unlocked RWMutex")`があります。HTTPハンドラの外側でpanicを拾っているから大丈夫、という前提は通用しません。`RLock`と`Unlock`、`Lock`と`RUnlock`を取り違えるとサーバーごと停止します。

### 使用開始後のロックの値コピーと排他制御の破綻

`Mutex`も`RWMutex`も、一度使ったあとにコピーしてはいけません。ドキュメントの「A Mutex must not be copied after first use.」がそれです。ロックを含む構造体を値レシーバのメソッドに渡したり、スライスに`append`したりすると、コピー先とコピー元で別々のロックになり、排他が静かに効かなくなります。

`go vet`のcopylocks解析がこれを`assignment copies lock value`として報告します。検出の仕組みと、`go test`の既定設定では素通りしてしまう範囲は[GoのDoNotCopy・DoNotCompare・DoNotImplementの解説](/tech/details/4485/)にまとめています。

## 単一ロックへの競合を減らす設計の選択肢

`Mutex`と`RWMutex`のどちらを選ぶかは、共有データを1本のロックで守ると決めた後の話です。その前に、競合そのものを減らす設計が3つあります。

1つ目はチャネルです。チャネル自身も内部にロックを持つので同期が消えるわけではありませんが、共有データを守るロックをアプリケーション側から無くせます。`sync`パッケージのドキュメント自身が「Higher-level synchronization is better done via channels and communication.」と述べているとおり、データを共有して守るのではなく、所有権を1本のゴルーチンに集めて受け渡す設計にすればロックは要りません。使い方は[Go言語のChannelとは](/tech/details/5061/)で整理しています。

2つ目は`sync.Map`です。ロックが消えるわけではなく（内部の`HashTrieMap`も`Mutex`を持ちます）、競合の粒度がマップ全体からノード単位へ細かくなります。表Cではその差が10倍以上として出ました。ただし型安全性が失われ、`any`との変換コストがかかるうえ、公式ドキュメントが通常のマップを推奨している点は変わりません。Go 1.24では`GOEXPERIMENT=nosynchashtriemap`で旧実装へ戻せましたが、本記事のgo1.26.5ではこの設定は削除されています。

3つ目はシャーディングです。キーのハッシュで16本や64本のロックに分散させれば、1本のロックに集中していた競合がそのまま分散します。自前で書く前に、キャッシュ用途なら[BigCacheの設計コンセプト](/tech/details/6634/)のような既存実装が同じ考え方を採っているので参考になります。同じキーに対する重複した重い処理をまとめたいだけなら、[singleflightによる重複呼び出し抑制](/tech/details/9058/)のほうが適します。

## データ競合とデッドロックの検出

ロックの掛け忘れは`go test -race`と`go build -race`のrace detectorが検出します。ただし検出できるのは実際に競合が起きた実行パスだけなので、テストがその並行パスを踏んでいなければ通り抜けます。常時有効にできないのはコストが理由で、[公式のData Race Detectorドキュメント](https://go.dev/doc/articles/race%5Fdetector)は「memory usage may increase by 5-10x and execution time by 2-20x」と書いています。CIの並行テストだけ`-race`を付け、本番ビルドでは外すのが現実的な落としどころです。

永久にブロックされたゴルーチンについては、Go 1.26で実験機能として入った`goroutineleak`プロファイルが、Go 1.27（2026年8月）で正式版になりました。[Go 1.27のリリースノート](https://go.dev/doc/go1.27)は「previously available as an experiment in Go 1.26, is now generally available.」と書いており、1.26で必要だった`GOEXPERIMENT=goroutineleakprofile`の指定は不要になっています。あわせて「A leaked goroutine is a goroutine blocked on some concurrency primitive (channels, sync.Mutex, sync.Cond, etc) that cannot possibly become unblocked.」と定義し、`runtime/pprof`から取得できるほか`net/http/pprof`の`/debug/pprof/goroutineleak`エンドポイントでも公開されると説明しています。検出はGCの到達可能性解析を使い、ブロック先の同期プリミティブがどの実行可能なゴルーチンからも到達できなければリークと判定する仕組みです。

この方式には明示された弱点があります。リリースノートは「the runtime may fail to identify leaks caused by blocking on concurrency primitives reachable through global variables or the local variables of runnable goroutines.」と書いています。パッケージレベルの`var mu sync.Mutex`はグローバル変数から常に到達可能なので、まさにこの検出漏れのパターンに当たります。構造体のフィールドにしても、その構造体がグローバル変数や実行可能なゴルーチンから到達可能なら、同じ検出漏れが起こり得ます。

なおデータベースのデッドロックは、ここで扱ったアプリ内のロックとは検出も解消も別物です。トランザクション同士が待ち合う場合の原因と対処は[データベースで起きるデッドロックの原因と解消](/tech/details/4122/)を参照してください。

## よくある質問

### mutexは何の略ですか？

mutual exclusion（相互排他）の略です。Goの標準ライブラリでも`sync.Mutex`のドキュメントは「A Mutex is a mutual exclusion lock.」と定義しています。読み方はミューテックスです。

### セマフォとミューテックスの違いは何ですか？

ミューテックスは同時に入れるのが常に1本であるのに対し、セマフォは同時実行数をNに制限する仕組みです。Goの`sync`パッケージが公開している型は`Cond`・`Locker`・`Map`・`Mutex`・`Once`・`Pool`・`RWMutex`・`WaitGroup`の8つで、セマフォ型は含まれません。同時実行数を絞りたい場合は容量Nのバッファ付きチャネルを使うか、`golang.org/x/sync/semaphore`の`Weighted`を使います。

### MutexとRWMutexはどちらが速いですか？

条件によって逆転します。go1.26.5・8物理コア／16論理CPUの実測では、読み取り100%なら並列度4で`RWMutex`が逆転し、並列度16では2.5倍速くなります。一方、並列度2までは`Mutex`のほうが速く、書き込みが1%混ざると並列度16でも`RWMutex`が負けます（220.2 ns対204.1 ns）。非競合では`RLock`と`RUnlock`の1往復が約31.6 ns、`Mutex`の`Lock`と`Unlock`が約36.6 nsで、`RWMutex`の`Lock`と`Unlock`だけが約62.8 nsと突出します。

### Lockしたゴルーチンと別のゴルーチンでUnlockしてよいですか？

できます。ドキュメントは「A locked Mutex is not associated with a particular goroutine. It is allowed for one goroutine to lock a Mutex and then arrange for another goroutine to unlock it.」と明記しています。`RWMutex`にも同じ記述があります。ただし解放し忘れると、そのロックを待つ処理が進まなくなるため、原則は同じ関数内で`defer`を使って閉じる書き方を勧めます。

### sync.Mutexは初期化が必要ですか？ポインタで持つべきですか？

初期化は不要です。「The zero value for a Mutex is an unlocked mutex.」のとおり、ゼロ値がそのまま未ロック状態です。持ち方は、守りたいデータと同じ構造体に値で埋め込み、その構造体自体をポインタで受け渡すのが定石です。構造体を値でコピーするとロックが分裂して排他が効かなくなります。

## 関連記事

- [ゴルーチン（Goroutine）とは？Go言語の並行処理を支える軽量スレッドの仕組みと使い方](/tech/details/4632/)
- [Go言語のChannelとは？その特徴と用途について徹底解説](/tech/details/5061/)
- [GoのDoNotCopy・DoNotCompare・DoNotImplementとは｜protobuf-goがコピー・比較・実装を抑止する仕組み](/tech/details/4485/)
- [Goのcontext canceledとdeadline exceededの切り分け｜原因の特定とWithDeadlineによる締切共有](/tech/details/3757/)
- [singleflightとは何か？Go言語における重複呼び出し抑制メカニズムの概要とパフォーマンス向上効果](/tech/details/9058/)

---

出典: [Goのsync.MutexとRWMutexの違いと使い分け｜実測ベンチで選ぶ排他制御](<https://www.issoh.co.jp/tech/details/5549/>)（株式会社一創）
