Go 并发编程中的计数器同步:Mutex、缓冲通道和非缓冲通道
在 Go 语言中构建并发应用程序时,同步至关重要,以确保安全地访问共享数据。Mutex
和 Channel
是 Go 中用于同步的主要工具。
本文探讨了构建安全并发计数器的几种方法。虽然参考文章使用 Mutex
解决了这个问题,但我们还将探讨使用缓冲通道和非缓冲通道的替代方法。
问题描述
我们需要构建一个可以安全地并发使用的计数器。
计数器代码
<code class="language-go">package main type Counter struct { count int } func (c *Counter) Inc() { c.count++ } func (c *Counter) Value() int { return c.count }</code>
为了确保代码的并发安全,让我们编写一些测试。
1. 使用 Mutex
Mutex
(互斥锁)是一种同步原语,它确保一次只有一个 goroutine 可以访问代码的关键部分。它提供了一种锁机制:当一个 goroutine 锁定 Mutex
时,其他试图锁定它的 goroutine 将被阻塞,直到 Mutex
被解锁。因此,当需要保护共享变量或资源免受竞争条件影响时,通常会使用它。
<code class="language-go">package main import ( "sync" "testing" ) func TestCounter(t *testing.T) { t.Run("using mutexes and wait groups", func(t *testing.T) { counter := Counter{} wantedCount := 1000 var wg sync.WaitGroup var mut sync.Mutex wg.Add(wantedCount) for i := 0; i < wantedCount; i++ { go func() { defer wg.Done() mut.Lock() counter.Inc() mut.Unlock() }() } wg.Wait() if counter.Value() != wantedCount { t.Errorf("got %d, want %d", counter.Value(), wantedCount) } }) }</code>
代码使用了 sync.WaitGroup
来跟踪所有 goroutine 的完成情况,并使用 sync.Mutex
来防止多个 goroutine 同时访问共享计数器。
2. 使用缓冲通道
通道是 Go 允许 goroutine 安全通信的一种方式。它们能够在 goroutine 之间传输数据,并通过控制对所传递数据的访问来提供同步。
在本例中,我们将利用通道来阻塞 goroutine,并只允许一个 goroutine 访问共享数据。缓冲通道具有固定的容量,这意味着它们可以在阻塞发送方之前容纳预定义数量的元素。只有当缓冲区已满时,发送方才会被阻塞。
<code class="language-go">package main import ( "sync" "testing" ) func TestCounter(t *testing.T) { t.Run("using buffered channels and wait groups", func(t *testing.T) { counter := Counter{} wantedCount := 1000 var wg sync.WaitGroup wg.Add(wantedCount) ch := make(chan struct{}, 1) ch <- struct{}{} // 允许第一个 goroutine 开始 for i := 0; i < wantedCount; i++ { go func() { defer wg.Done() <-ch counter.Inc() ch <- struct{}{} }() } wg.Wait() if counter.Value() != wantedCount { t.Errorf("got %d, want %d", counter.Value(), wantedCount) } }) }</code>
代码使用容量为 1 的缓冲通道,允许一次只有一个 goroutine 访问计数器。
3. 使用非缓冲通道
非缓冲通道没有缓冲区。它们会阻塞发送方,直到接收方准备好接收数据。这提供了严格的同步,其中数据一次一个地传递到 goroutine 之间。
<code class="language-go">package main import ( "sync" "testing" ) func TestCounter(t *testing.T) { t.Run("using unbuffered channels and wait groups", func(t *testing.T) { counter := Counter{} wantedCount := 1000 var wg sync.WaitGroup wg.Add(wantedCount) ch := make(chan struct{}) go func() { for i := 0; i < wantedCount; i++ { ch <- struct{}{} } close(ch) }() for range ch { counter.Inc() wg.Done() } if counter.Value() != wantedCount { t.Errorf("got %d, want %d", counter.Value(), wantedCount) } }) } </code>
代码使用非缓冲通道,确保一次只有一个 goroutine 访问计数器。
4. 使用缓冲通道,不使用 WaitGroup
我们还可以使用缓冲通道而不使用 WaitGroup
,例如使用无限循环或另一个通道来跟踪 goroutine 的完成情况。
结论
本文探讨了在 Go 中构建安全并发计数器的不同方法。掌握这些工具以及何时使用它们是编写高效且安全的并发 Go 程序的关键。
参考资源
本文受《Learn Go with tests》中同步章节的启发。
希望本文对您有所帮助!
以上是Go 并发:互斥体与通道的示例的详细内容。更多信息请关注PHP中文网其他相关文章!