How does a mutex.Lock() know which variables to lock?
Asked Answered
S

2

9

I'm a go-newbie, so please be gentle.

So I've been using mutexes in some of my code for a couple weeks now. I understand the concept behind it: lock access to a certain resource, interact with it (read or write), and then unlock it for others again.

The mutex code I use is mostly copy-paste-adjust. The code runs, but I'm still trying to wrap my head around it's internal working. Until now I've always used a mutex within a struct to lock the struct. Today I found this example though, which made it completely unclear for me what the mutex actually locks. Below is a piece of the example code:

var state = make(map[int]int)

var mutex = &sync.Mutex{}

var readOps uint64
var writeOps uint64

// Here we start 100 goroutines to execute repeated reads against the state, once per millisecond in each goroutine.
for r := 0; r < 100; r++ {
    go func() {
        total := 0
        for {
            key := rand.Intn(5)
            mutex.Lock()
            total += state[key]
            mutex.Unlock()
            atomic.AddUint64(&readOps, 1)

            time.Sleep(time.Millisecond)
        }
    }()
}

What puzzles me here is that there doesn't seem to be any connection between the mutex and the value it is supposed to lock. Until today I thought that the mutex can lock a specific variable, but looking at this code it seems to somehow lock the whole program into doing only the lines below the lock, until the unlock is ran again. I suppose that means that all the other goroutines are paused for a moment until the unlock is ran again. Since the code is compiled I suppose it can know what variables are accessed between the lock() and the unlock(), but I'm not sure if that is the case.

If all other programs pause for a moment, it doesn't sound like real multi-processing, so I'm guessing I don't have a good understanding of what's going on.

Could anybody help me out in understanding how the computer knows which variables it should lock?

Shuttering answered 22/6, 2019 at 21:39 Comment(3)
I can't answer for Go, but locking generally does not lock a variable: it simply add a barrier for thread that try to enter some specific part ("protected" by the mutex). And there are different kind of barrier.Highway
A mutex locks itself and nothing more. It’s a primitive with the same semantics in most every language, there’s no other magic involved and there’s nothing special about them in Go.Hedvah
...and so, how you use them is, if there is some variable/object or group of variables/objects that need to be protected, you make sure that every part of your code that accesses them is guarded by a mutex.Fug
C
14

lock access to a certain resource, interact with it (read or write), and then unlock it for others again.

Basically yes.

What puzzles me here is that there doesn't seem to be any connection between the mutex and the value it is supposed to lock.

Mutex is just a mutual exclusion object that synchronizes access to a resource. That means, if two different goroutines want to lock the mutex, only the first can access it. The second goroutines now waits indefinitely until it can itself lock the mutex. There is no connection to variables whatsoever, you can use mutex however you want. For example only one http request, only one database read/write operation or only one variable assignment. While i don't advice the usage of mutex for those examples, the general idea should become clear.

but looking at this code it seems to somehow lock the whole program into doing only the lines below the lock, until the unlock is ran again.

Not the whole program, only every goroutine who wants to access the same mutex waits until it can.

I suppose that means that all the other goroutines are paused for a moment until the unlock is ran again.

No, they don't pause. They execute until they want to access the same mutex.

If you want to group your mutex specifically with a variable, why not create a struct?

Crick answered 22/6, 2019 at 23:37 Comment(4)
Aha! Now I finally got it; not the variable but the mutex is locked!Shuttering
This is the explanation I was looking for! So, it's all about the mutex's "flag"!Archine
Thank you very much for the detailed explanation of how mutex works. but I have some doubts wrt: If you want to group your mutex specifically with a variable, why not create a struct? I created a testStruct with sync.Mutex in it. I found that 1. mutex declared within a struct can be used to lock other variables not belonging to struct also. go.dev/play/p/34DiWgcZQot 2. calling Lock() method of mutex inside a struct in one routine doesn't stop other routines from accessing the variables of that struct. go.dev/play/p/SUs-mRhFozYKingofarms
So whats the question? You observed both of your examples correctly.Crick
C
0

In fact a mutex doesn't lock variables or data but code regions (placed between Lock() and Unlock() function calls). A mutext doesn't prevent several goroutine from accessing the same data at the same time but from running the same code section at the same time. A mutex doesn't care about variables, data or memory location ...

A mutex usually contains a variable that indicate whether the mutex is locked or not. To simplify you can imagine it like an integer that can have two value:

  • 0 : The mutex is locked. No goroutine except the one that locked it is allowed to run code sections protected by the mutex
  • 1 : The mutex is unlocked, and every goroutine is allowed to lock the mutex and run protected code sections.

When a goroutine wants to run a code section protected by an unlocked mutex, the state is switched from 1 to 0 atomically. Once the goroutine has finished, the mutex is unlocked and the state switch atomically from 0 to 1.

Let's see an example:

mapp := make(map[int]int)

func funcA () {
  var mutexA sync.Mutex
  mutexA.Lock()
  for i := 1; i < 1000; i++ {
    mapp[i] = i
  }
  mutexA.Unlock()
}

func funcB () {
 var mutexB sync.Mutex
 mutexB.Lock()
 for i := 1; i < 1000; i++ {
    mapp[i] = i + 1
 }
 mutexB.Unlock()
}

go funcA()
go funcB()

This code is not protected from data race because mapp mapping variable can be edited by 2 different goroutines at the same time since both have 2 different mutex protecting 2 different code sections. The mutexA protects only the critical code section of funcA function whereas the mutexB protects only the critical code section of funcB function.

We have 2 goroutine handling the same data but via 2 different functions. How to fix this ?

By using the same mutex in both goroutines:

mapp := make(map[int]int)
var commonMutex sync.Mutex

func funcA () { 
  commonMutex.Lock()
  for i := 1; i < 1000; i++ {
    mapp[i] = i
  }
  commonMutex.Unlock()
}

func funcB () {
 commonMutex.Lock()
 for i := 1; i < 1000; i++ {
    mapp[i] = i + 1
 }
 commonMutex.Unlock()
}

go funcA()
go funcB()

When a mutex is locked, all code sections it protects ( between Lock() and Unlock() ) are locked too. Now when goroutine A lock commonMutex mutex, goroutine B can't enter in the funcB's for loop before goroutine A unlock and reverse.

That's why a mutex is often declared as a struct field. It allows to protect the same data in several functions.

Claimant answered 12/2, 2024 at 22:37 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.