muiiu
muiiu

Reputation: 587

When should you use a mutex over a channel?

For the past few weeks I've been wrestling with one (not-so) simple question:

When is it best to use sync.Mutex and, conversely, when is it best use a chan?

It seems that for a lot of problems either strategy is interchangeable with the other - and that's just the problem!

Take this video found in the Golang documentation.
Below, I've taken the liberty to dictate the code in the playground and also translate it to a sync.Mutex equivalent.

Is there a certain problem - encountered in the real world - that warrants the use of one other?

Notes:

Playgrounds:

Ping/pong with chan:

package main

import (
    "fmt"
    "time"
)

type Ball struct { hits int }

func main() {
    table := make(chan *Ball)
    go player("ping", table)
    go player("pong", table)
    
    table <- new(Ball)
    time.Sleep(1 * time.Second)
    <-table
}

func player(name string, table chan *Ball) {
    for {
        ball := <-table
        ball.hits++
        fmt.Println(name, ball.hits)
        time.Sleep(100 * time.Millisecond)
        table <- ball
    }
}

Ping/pong with sync.Mutex:

package main

import (
    "fmt"
    "time"
    "sync"
)

type Ball struct { hits int }

var m =  sync.Mutex{}

func main() {
    ball := new(Ball)
    go player("ping", ball)
    go player("pong", ball)
    
    time.Sleep(1 * time.Second)
}

func player(name string, ball *Ball) {
    for {
        m.Lock()
        ball.hits++
        fmt.Println(name, ball.hits)
        time.Sleep(100 * time.Millisecond)
        m.Unlock()

    }
}

Upvotes: 28

Views: 25311

Answers (2)

wasmup
wasmup

Reputation: 16233

Some example channel usecases:

  • Limit concurrent tasks number (e.g. number of running goroutines) using buffered channel capacity (and/or length)
  • Transferring ownership of an object (one and only one goroutine writes to it while no other goroutines reading it)
  • Coordination, synchronization, sending signals, and data.

Some example primitives usecases:

  • Guarding internal state of a struct (with e.g. sync.Mutex, or sync.RWMutex)
  • Performance critical (algorithm and use case dependent, not a general rule)

Examples

To make it clear, imagine we need a one second counter, so in the following examples we count for a second then print the counter value to see how fast it counts:

No |         Count |       Method
------------------------------------------------------
 1 |     17_729_027 | Using sync.RWMutex for increment   
 2 |     12_180_741 | Using channel for increment    
 3 |    106_743_095 | Using channel for timer 
 4 |    104_178_671 | Using time.AfterFunc and channel sync

Note: go version go1.13.5 linux/amd64


Codes:

1 - Using sync.RWMutex for increment:

package main

import (
    "sync"
    "time"
)

func main() {
    var i rwm
    go func() {
        for {
            i.inc() // free running counter
        }
    }()
    time.Sleep(1 * time.Second)
    println(i.read()) // sampling the counter
}

type rwm struct {
    sync.RWMutex
    i int
}

func (l *rwm) inc() {
    l.Lock()
    defer l.Unlock()
    l.i++
}
func (l *rwm) read() int {
    l.RLock()
    defer l.RUnlock()
    return l.i
}

2 - Using channel for increment:

package main

import (
    "time"
)

func main() {
    ch := make(chan int, 1)
    ch <- 1
    timeout := time.NewTimer(1 * time.Second)
loop:
    for {
        select {
        case <-timeout.C:
            timeout.Stop()
            break loop
        default:
            ch <- 1 + <-ch
        }
    }

    println(<-ch)
}

3 - Using channel for timer:

package main

import "time"

func main() {
    ch := make(chan int)
    go func() {
        timeout := time.NewTimer(1 * time.Second)
        defer timeout.Stop()
        i := 1
        for {
            select {
            case <-timeout.C:
                ch <- i
                return
            default:
                i++
            }
        }
    }()

    println(<-ch)
}

4 - Using time.AfterFunc and channel sync:

package main

import (
    "fmt"
    "time"
)

func main() {
    d := 1 * time.Second
    i := uint64(0)
    ch := make(chan struct{})

    time.AfterFunc(d, func() {
        close(ch)
    })

loop:
    for {
        select {
        case <-ch:
            break loop
        default:
            i++
        }
    }

    fmt.Println(i) // 104_178_671
}

Upvotes: 3

Jerry An
Jerry An

Reputation: 1432

In Go, channels are fantastic, and you can use them to communicate between goroutines. However, you may want to use the sync.Mutex in some circumstances for convenience. These circumstances are like the following:

  • Guarding an internal state
  • Cache problems
  • For better performance

Here are three examples and explanations

  1. A simple counter

enter image description here

  1. Ping Pong Game

enter image description here

  1. The Simplest Cache

enter image description here

Upvotes: 18

Related Questions