15

I'm relatively new to Go, and I need to make a variable thread-safe. I know in java you can just use the synchronized keyword, but nothing like that seems to exist in go. Is there any way to synchronize variables?

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Pika Supports Ukraine
  • 3,612
  • 10
  • 26
  • 42
  • 2
    1) What kind of "thread-safety" is required/desired? 2) Look at https://golang.org/pkg/sync/ ? – user2864740 Oct 17 '18 at 20:48
  • 5
    `synchronized` in Java is a mean to allow only a single thread to execute a code block. In Go there are numerous constructs to achieve that (e.g. mutexes, channels, waitgroups), but Go's proverb is: _"Do not communicate by sharing memory; instead, share memory by communicating."_ So instead of locking and sharing a variable, try to not do that but instead communicate the result between goroutines e.g. using channels (so you won't have to access shared memory). For details, see https://blog.golang.org/share-memory-by-communicating – icza Oct 17 '18 at 20:53
  • 1
    Give some example code here and people can suggest a mechanism. There is no way to do this at a variable declaration, but you probably don't need to. – Kenny Grant Oct 18 '18 at 05:59
  • @icza, I'd say it'd be better to convert your comment to an answer. The other two are not too good: one focuses on a particular solution too much and another is simply a glib response to a phrase ripped out of context. – kostix Oct 18 '18 at 15:36
  • @kostix Did so, but it ended up longer than I thought it would. Obviously the topic is even bigger, maybe I improve it tomorrow. – icza Oct 18 '18 at 20:49
  • "thread safe" isn't a well-defined term, you must describe what you mean by "thread safe". – Lasse V. Karlsen Oct 19 '18 at 10:45

1 Answers1

34

synchronized in Java is a mean to allow only a single thread to execute a code block (at any given time).

In Go there are numerous constructs to achieve that (e.g. mutexes, channels, waitgroups, primitives in sync/atomic), but Go's proverb is: "Do not communicate by sharing memory; instead, share memory by communicating."

So instead of locking and sharing a variable, try to not do that but instead communicate the result between goroutines e.g. using channels (so you won't have to access shared memory). For details, see The Go Blog: Share Memory By Communicating.

Of course there may be cases when the simplest, direct solution is to use a mutex to protect concurrent access from multiple goroutines to a variable. When this is the case, this is how you can do that:

var (
    mu        sync.Mutex
    protectMe int
)

func getMe() int {
    mu.Lock()
    me := protectMe
    mu.Unlock()
    return me
}

func setMe(me int) {
    mu.Lock()
    protectMe = me
    mu.Unlock()
}

The above solution could be improved in several areas:

  • Use sync.RWMutex instead of sync.Mutex, so that the getMe() may lock for reading only, so multiple concurrent readers would not block each other.

  • After a (successful) locking it is advisable to unlock using defer, so if something bad happens in the subsequent code (e.g. runtime panic), the mutex will still be unlocked, avoiding resource leaks and deadlocks. Although this example is so simple, nothing bad could happen and does not warrant unconditional use of deferred unlocking.

  • It is good practice to keep the mutex close to the data it is ought to protect. So "wrapping" protectMe and its mu in a struct is a good idea. And if we're at it, we may also use embedding, so locking / unlocking becomes more convenient (unless this functionality must not be exposed). For details, see When do you embed mutex in struct in Go?

So an improved version of the above example could look like this (try it on the Go Playground):

type Me struct {
    sync.RWMutex
    me int
}

func (m *Me) Get() int {
    m.RLock()
    defer m.RUnlock()
    return m.me
}

func (m *Me) Set(me int) {
    m.Lock()
    m.me = me
    m.Unlock()
}

var me = &Me{}

func main() {
    me.Set(2)
    fmt.Println(me.Get())
}

This solution has another advantage: should you need multiple values of Me, it will automatically have different, separate mutexes for each value (our initial solution would require creating separate mutexes manually for each new values).

Although this example is correct and valid, may not be practical. Because protecting a single integer does not really require a mutex. We could achieve the same using the sync/atomic package:

var protectMe int32

func getMe() int32 {
    return atomic.LoadInt32(&protectMe)
}

func setMe(me int32) {
    atomic.StoreInt32(&protectMe, me)
}

This solution is shorter, cleaner and faster. If you're goal is only to protect a single value, this solution is preferred. If the data structure you ought to protect is more complex, atomic may not even be viable, and using a mutex might be justified.

Now after showing examples of sharing / protecting variables, we should also give an example what we should aim to achieve to live up to "Do not communicate by sharing memory; instead, share memory by communicating."

The situation is that you have multiple concurrent goroutines, and you use a variable where you store some state. One goroutine changes (sets) the state, and another reads (gets) the state. To access this state from multiple goroutines, access must be synchronized.

And the idea is to not have a "shared" variable like this, but instead the state that one goroutine would set, it should "send" it instead, and the other goroutine that would read it, it should be the one the state is "sent to" (or in other words, the other goroutine should receive the changed state). So there is no shared state variable, instead there is a communication between the 2 goroutines. Go provides excellent support for this kind of "inter-goroutine" communication: channels. Support for channels is built into the language, there are send statements, receive operators and other support (e.g. you can loop over the values sent on a channel). For an intro and details, please check this answer: What are channels used for?

Let's see a practical / real-life example: a "broker". A broker is an entity where "clients" (goroutines) may subscribe to receive messages / updates, and the broker is capable of broadcasting messages to subscribed clients. In a system where there are numerous clients that might subscribe / unsubscribe at any time, and there may be a need to broadcast messages at any time, synchronizing all this in a safe manner would be complex. Wisely using channels, this broker implementation is rather clean and simple. Please allow me to not repeat the code, but you can check it in this answer: How to broadcast message using channel. The implementation is perfectly safe for concurrent use, supports "unlimited" clients, and does not use a single mutex or shared variable, only channels.

Also see related questions:

Reading values from a different thread

icza
  • 389,944
  • 63
  • 907
  • 827
  • 2
    A nice overview! May I suggest renaming the mutex's field to just `mu` and putting it above the variable it protects? This convention is not for some reason listed [in _the guide_](https://github.com/golang/go/wiki/CodeReviewComments) but is spelled explicitly in [_The Book_](http://www.gopl.io) ;-) Basically, a mutex variable (barring the need to have more than one in a struct) is named `mu` and is put above the set of fields it protects (all possibly preceded by a blank line). – kostix Oct 19 '18 at 10:48
  • @kostix Thanks. Done with some other minor adjustments. I was already grouping the mutex with what it protects in the variable declaration, but yes, listing the mutex first is handy. Also `mu` is the most common name used for short mutex name. – icza Oct 19 '18 at 11:14
  • @icza Thx for that detailed example! However there is an error in `Get()` method: You should not use `m.RUnlock()` but use `defer m.RUnlock()` , otherwise this method does not do what it is intended for. I know that the answer is 5y.o. but wanted to make sure that other people would not copy this mistake. – Alex Sep 05 '21 at 03:40
  • @AlexeyGalishnikov You're right, it was left out accidentally. Fixed it, thanks. – icza Sep 05 '21 at 06:41
  • 1
    I thought this was generated by chatGPT it's so thorough, but it was posted in 2018 . Nice work! – Kyle Chadha Mar 15 '23 at 23:21