Home >Backend Development >Golang >Go Concurrency: Mutexes vs Channels with Examples

Go Concurrency: Mutexes vs Channels with Examples

Mary-Kate Olsen
Mary-Kate OlsenOriginal
2025-01-08 20:10:54540browse

Counter synchronization in Go concurrent programming: Mutex, buffered channels and unbuffered channels

When building concurrent applications in Go, synchronization is crucial to ensure secure access to shared data. Mutex and Channel are the main tools for synchronization in Go.

This article explores several ways to build safe concurrency counters. While the reference article solves this problem using Mutex, we'll also explore alternatives using buffered and unbuffered channels.

Problem Description

We need to build a counter that can be safely used concurrently.

Counter code

<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>

To make our code concurrency safe, let’s write some tests.

1. Use Mutex

Mutex (mutex) is a synchronization primitive that ensures that only one goroutine can access critical parts of the code at a time. It provides a locking mechanism: when a goroutine locks Mutex, other goroutines trying to lock it will be blocked until Mutex is unlocked. Therefore, it is often used when a shared variable or resource needs to be protected from race conditions.

<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>

The code uses sync.WaitGroup to track the completion of all goroutines, and uses sync.Mutex to prevent multiple goroutines from accessing the shared counter at the same time.

2. Use buffer channel

Go Concurrency: Mutexes vs Channels with Examples

Channels are a way for Go to allow goroutines to communicate securely. They are able to transfer data between goroutines and provide synchronization by controlling access to the passed data.

In this example, we will use channels to block goroutines and allow only one goroutine to access the shared data. Buffered channels have a fixed capacity, meaning they can hold a predefined number of elements before blocking the sender. The sender will only block when the buffer is full.

<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>

The code uses a buffered channel with a capacity of 1, allowing only one goroutine to access the counter at a time.

3. Use non-buffered channels

Go Concurrency: Mutexes vs Channels with Examples

Unbuffered channels have no buffers. They block the sender until the receiver is ready to receive data. This provides strict synchronization, where data is passed between goroutines one at a time.

<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>

The code uses unbuffered channels to ensure that only one goroutine accesses the counter at a time.

4. Use buffer channel instead of WaitGroup

We can also use buffered channels without WaitGroup, for example using an infinite loop or another channel to track the completion of the goroutine.

Conclusion

This article explores different approaches to building safe concurrency counters in Go. Knowing these tools and when to use them is key to writing efficient and safe concurrent Go programs.

Reference Resources

This article is inspired by the synchronization chapter in "Learn Go with tests".

Hope this article is helpful to you!

The above is the detailed content of Go Concurrency: Mutexes vs Channels with Examples. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn