Xuesong Ye
Xuesong Ye

Reputation: 515

How to avoid race conditions when modifying slices in Go?

func sample(testList []struct{}, testMap map[int64]struct{}) {
    for i, test := range testList {
        // some if conditions to get the matched key
        testList[i] = testMap[key]
    }
}

The value of map and slice are the same type. I will use some matched map values to replace the values in the slice.

Upvotes: 1

Views: 1330

Answers (1)

kostix
kostix

Reputation: 55443

In Go, there are two aspects to protecting concurrent access to slices.

First, a slice is a contiguous memory block which contains zero or more elements of the same type; each is accessed by its index (0 to len(slice)-1).
In Go, each element of a slice is treated as an independent variable, which means it's OK to concurrently access/modify different elements of the same slice.
To reiterate, it's OK to write to the elements at indexes N and M of the same slice from different concurrently running goroutines as long as N != M for every such access.

Hence, you only need to serialize concurrent accesses performed by multiple goroutines to the same element of a slice.
In other words, if you need to read and/or modify an element at index N from multiple concurrently executing goroutines you need to protect that element by a mutex.

Now note that in real life the need to protect accesses to individual elements by individual mutexes happens reasonably seldom, so usually one just uses a single mutex to protect access to any element of a slice.

Second, some operations on a slice may reallocate the memory block which contains the slice's elements, and here it gets a bit tricky: say, when you do

slice = append(slice, a_new_element)

and slice has no room to hold a_new_element, two things will happen:

  1. append will allocate a new memory block to hold len(slice)+1 elements and then copy the memory of the original memory block there.
  2. append will return a new slice descriptor and the contents of the slice variable will be overwritten with it by the = operator.

All these operations will naturally race with any access to that slice which involves indexing (like slice[N]): for instance, an attempt to update a slice's element at a particular index performed by a goroutine might happen at the same time the goroutine performing append will be copying the slice's memory.

From this follows, that any operation on a slice which might reallocate the slice's memory block and any update of the variable(s) holding the slice's descriptor must be synchronized with all the goroutines which are modifying the slice's elements—no matter whether those goroutines are synchronized with regard to modifying individual slice's elements.

TL;DR

Use a single mutex to protect any kind of access to a slice if you're about to access it from more than a single goroutine.

If you will eventually detect that mutex will have become a bottleneck, you have ways to lower the contention using the logic described above.

Upvotes: 9

Related Questions