同期
Sync
安全に併用できる並行処理を作りたい。
安全でない並行処理から始めて、その動作がシングルスレッド環境で機能することを確認します。
次に、複数のゴルーチンがテストを介してそれを使用して修正することで、安全でないことを実行します。
APIで、カウンターをインクリメントしてその値を取得するメソッドを提供する必要があります。
func TestCounter(t *testing.T) {
t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) {
counter := Counter{}
counter.Inc()
counter.Inc()
counter.Inc()
if counter.Value() != 3 {
t.Errorf("got %d, want %d", counter.Value(), 3)
}
})
}
./sync_test.go:9:14: undefined: Counter
Counter
を定義しましょう。type Counter struct {
}
再試行して、次のように失敗します。
./sync_test.go:14:10: counter.Inc undefined (type Counter has no field or method Inc)
./sync_test.go:18:13: counter.Value undefined (type Counter has no field or method Value)
最終的にテストを実行するために、これらのメソッドを定義できます。
func (c *Counter) Inc() {
}
func (c *Counter) Value() int {
return 0
}
実行して失敗するはずです。
=== RUN TestCounter
=== RUN TestCounter/incrementing_the_counter_3_times_leaves_it_at_3
--- FAIL: TestCounter (0.00s)
--- FAIL: TestCounter/incrementing_the_counter_3_times_leaves_it_at_3 (0.00s)
sync_test.go:27: got 0, want 3
これは、私たちのようなGoの専門家にとっては簡単なことです。データ型のカウンターの状態を保持し、
Inc
を呼び出すたびにインクリメントする必要がありますtype Counter struct {
value int
}
func (c *Counter) Inc() {
c.value++
}
func (c *Counter) Value() int {
return c.value
}
リファクタリングすることはそれほど多くありませんが、
Counter
を中心にさらに多くのテストを作成するので、テストが少し明確に読み取れるように、小さなアサーション関数assertCount
を作成します。t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) {
counter := Counter{}
counter.Inc()
counter.Inc()
counter.Inc()
assertCounter(t, counter, 3)
})
func assertCounter(t *testing.T, got Counter, want int) {
t.Helper()
if got.Value() != want {
t.Errorf("got %d, want %d", got.Value(), want)
}
}
それは十分に簡単でしたが、現在は、並行環境で使用しても安全である必要があるという要件があります。これを実行するには、失敗するテストを作成する必要があります。
t.Run("it runs safely concurrently", func(t *testing.T) {
wantedCount := 1000
counter := Counter{}
var wg sync.WaitGroup
wg.Add(wantedCount)
for i := 0; i < wantedCount; i++ {
go func(w *sync.WaitGroup) {
counter.Inc()
w.Done()
}(&wg)
}
wg.Wait()
assertCounter(t, counter, wantedCount)
})
これは、
wantedCount
をループし、goroutineを起動して、counter.Inc()
を呼び出します。WaitGroup
は、ゴルーチンのコレクションが完了するのを待ちます。メインのゴルーチンはAdd
を呼び出して、待機するゴルーチンの数を設定します。次に、各ゴルーチンが実行され、完了したらDone
を呼び出します。同時に、すべてのゴルーチンが完了するまで、Wait
を使用してブロックすることができま す。
アサーションを作成する前に
wg.Wait()
が完了するのを待つことで、すべてのゴルーチンがCounter
をInc
しようとしたことを確認できます。=== RUN TestCounter/it_runs_safely_in_a_concurrent_envionment
--- FAIL: TestCounter (0.00s)
--- FAIL: TestCounter/it_runs_safely_in_a_concurrent_envionment (0.00s)
sync_test.go:26: got 939, want 1000
FAIL
テストは別の数値で おそらく 失敗しますが、それでも複数のゴルーチンが同時にカウンターの値を変更しようとしている場合は機能しないことを示しています。
Mutex
は相互排他ロックです。ミューテックスのゼロ値は、ロックされていないミューテックスです。
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
これが意味することは、
Inc
を呼び出すgoroutineが最初にある場合、Counter
のロックを取得することです。他のすべてのゴルーチンは、アクセスを取得する前に、それがUnlock
されるのを待つ必要があります。ここでテストを再実行すると、変更を行う前に各ゴルーチンが順番を待たなければならないため、テストに合格するはずです。
あなたはこのような例を見るかもしれません
type Counter struct {
sync.Mutex
value int
}
それはコードをもう少しエレガントにすることができると主張することができます。
func (c *Counter) Inc() {
c.Lock()
defer c.Unlock()
c.value++
}
これは見栄えが良いですが、プログラミングは非常に主観的な分野ですが、これは悪いことであり間違っています。
場合によっては、型の埋め込みがその型のメソッドが public インターフェースの一部になることを忘れることがあります。そしてあなたはしばしばそれを望まないでしょう。 公開APIには細心の注意を払う必要があることを忘れないでください。何かを公開する瞬間は、他のコードがそれに結合できる瞬間です。私たちは常に不必要な結合を避けたいと思っています。
Lock
とUnlock
を公開することはせいぜい混乱を招きますが、最悪の場合、同じタイプの呼び出し元がこれらのメソッドの呼び出しを開始すると、ソフトウェアに非常に有害な可能性があります。
このAPIのユーザーがロックの状態を誤って変更する方法を示します
これは本当に悪い考えのようです
テストはパスしましたが、コードはまだ少し危険です
コードで
go vet
を実行すると、次のようなエラーが表示されますsync/v2/sync_test.go:16: call of assertCounter copies lock value: v1.Counter contains sync.Mutex
sync/v2/sync_test.go:39: assertCounter passes lock by value: v1.Counter contains sync.Mutex
ミューテックスは、最初の使用後にコピーしてはなりません。
Counter
(by value)をassertCounter
に渡すと、ミューテックスのコピーが作成されます。これを解決するには、代わりに
Counter
へのポインターを渡す必要があるため、assertCounter
のシグネチャを変更しますfunc assertCounter(t *testing.T, got *Counter, want int)
*Counter
ではなくCounter
を渡そうとしているため、テストはコンパイルされなくなりました。これを解決するには、自分で型を初期化しない方がよいことをAPIのリーダーに示すコンストラクタを作成することをお勧めします。func NewCounter() *Counter {
return &Counter{}
}
Counter
を初期化するときに、この関数をテストで使用します。Mutex
を使用すると、データにロックを追加できますWaitgroup
は、ゴルーチンがジョブを完了するのを待つ手段です
私たちは以前に最初の並行性の章でゴルーチンをカバーしましたこれで安全な並行コードを書くことができるので、なぜロックを使うのでしょうか? go wikiには、このトピック専用のページがあります。ミューテックスまたはチャネル
Goの初心者によくある間違いは、それが可能であったり、楽しいからといって、チャネルやゴルーチンを使いすぎてしまうことです。sync.Mutex
が問題に最も適している場合は、恐れずに同期を使用してください。 Goは、問題を最もよく解決するツールを使用できるようにし、1つのスタイルのコードに強制するのではなく、実用的です。
言い換え
- データの所有権を渡すときにチャネルを使用する
- 状態の管理にミューテックスを使用する
ビルドスクリプトで
go vet
を使用することを忘れないでください。貧弱なユーザーに影響が及ぶ前に、コード内のいくつかの微妙なバグを警告することができます。- 埋め込みがパブリックAPIに与える影響について考えてください。
- これらのメソッドを 本当に 公開したいですか?
- ミューテックスに関しては、これは非常に予 測不能で奇妙な方法で潜在的に悲惨なものになる可能性があります。あるべきでないミューテックスをロック解除するいくつかの悪意のあるコードを想像してください。これは非常に奇妙なバグを引き起こし、追跡が困難になります。
最終更新 1yr ago