Golang synchronization

From wikinotes
Revision as of 04:32, 17 February 2023 by Will (talk | contribs) (→‎Atomics)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Synchronization tools are used to synchronize multiple concurrent codepaths so that your program can continue synchronously.

Documentation

sync docs https://pkg.go.dev/sync

Atomics

import "sync/atomic"

bool := atomic.Bool{}
prevVal : = bool.Swap(true)  // assigns true, returns prev-value
bool.Store(false)            // assigns true

Mutexes

Mutex

A lock used to synchronize threads. No wait-time is specified, you are responsible for re-trying the lock if desired.

var lock = Mutex{}

lock.Lock()
lock.Unlock()
lock.TryLock()  // discouraged

RWMutex

Variation of a mutex where:

  • any number of items can acquire a lock for reading
  • only one item can acquire lock for writing
  • you cannot write while any other lock is reading
import "sync"

var lock = RWMutex{}

// lock/unlock for reading
lock.RLock()
lock.RUnlock()

// lock/unlock for writing
lock.Lock()
lock.Unlock()

It would be sensible to unlcok in deferred functions.

WaitGroups

Basics

WaitGroups are global threadsafe counters that are incremented/decremented.
You can think of them as semaphores.

wg = sync.WaitGroup{}

wg.Add(3)  // add 3x increments to countdown
wg.Done()  // decrement waitgroup by one
wg.Wait()  // wait for waitgroup to reach 0
// wait-groups are threadsafe proxy objects, intended to be globally accessible
var wg = sync.WaitGroup{}

func printHi() {
    fmt.Println("hi")
    wg.Done()
}

func main() {
    wg.Add(3)
    for i=0; i<3; i++ {
        go printHi()
    }
    wg.Wait()
    fmt.Println("bye")
}

Debugging

Inspecting the waitgroup struct in a debugger can be very helpful.
Notably, you can tell if a WaitGroup has satisfied all wait conditions,
and even have a rough idea how many conditions it is still waiting for.

Remaining Waits

How to debug, recap

dlv test foo.com/x/project/./internal/pkg -- -test.run TestSomething  # start debugger
c                                                                     # start executing
p wg                                                                  # pring your WaitGroup value

Comparing values, you can see state1 is multiples or the original value.

// new/satisfied waitgroup            (4294967296 * 0 == 0)
sync.WaitGroup {
    noCopy: sync.noCopy {},
    state1: 0,
    state2: 1,
}

// wait 1x 'wg.Done()'                (4294967296 * 1 == 4294967296)
sync.WaitGroup {
        noCopy: sync.noCopy {},
        state1: 4294967296,
        state2: 0,}

// wait on 2x 'wg.Done()'             (4294967296 * 2 == 8589934592)
sync.WaitGroup {
        noCopy: sync.noCopy {},
        state1: 8589934592,
        state2: 0,}

// wait on 3x 'wg.Done()'             (4294967296 * 3 == 12884901888)
sync.WaitGroup {
        noCopy: sync.noCopy {},
        state1: 12884901888,
        state2: 0,}

You can poke in the sourcecode for the details, but even without it you can use this to get a rough idea of what state your waitgroup is in and when.

Memory Address

Within golang delve, you can find out if your object is pointing to the same memory address.

p &wg   # memory addr of object
p &*wg  # memory addr of obj referenced by pointer