Can I concurrently write different slice elements
Asked Answered
R

4

54

I have a slice that contains work to be done, and a slice that will contain the results when everything is done. The following is a sketch of my general process:

var results = make([]Result, len(jobs))
wg := sync.WaitGroup{}
for i, job := range jobs {
    wg.Add(1)
    go func(i int, j job) {
        defer wg.Done()
        var r Result = doWork(j)
        results[i] = r
    }(i, job)
}
wg.Wait()
// Use results

It seems to work, but I have not tested it thoroughly and am not sure if it is safe to do. Generally I would not feel good letting multiple goroutines write to anything, but in this case, each goroutine is limited to its own index in the slice, which is pre-allocated.

I suppose the alternative is collecting results via a channel, but since order of results matters, this seemed rather simple. Is it safe to write into slice elements this way?

Republicanism answered 17/4, 2018 at 13:23 Comment(1)
Note that you can do a quick check for such code by wrapping into a test and running it via go test -race -run NameOfThatTestFunc . — if it detects a data race, it panics.Bardo
G
71

The rule is simple: if multiple goroutines access a variable concurrently, and at least one of the accesses is a write, then synchronization is required.

Your example does not violate this rule. You don't write the slice value (the slice header), you only read it (implicitly, when you index it).

You don't read the slice elements, you only modify the slice elements. And each goroutine only modifies a single, different, designated slice element. And since each slice element has its own address (own memory space), they are like distinct variables. This is covered in Spec: Variables:

Structured variables of array, slice, and struct types have elements and fields that may be addressed individually. Each such element acts like a variable.

What must be kept in mind is that you can't read the results from the results slice without synchronization. And the waitgroup you used in your example is a sufficient synchronization. You are allowed to read the slice once wg.Wait() returns, because that can only happen after all worker goroutines called wg.Done(), and none of the worker goroutines modify the elements after they called wg.Done().

For example, this is a valid (safe) way to check / process the results:

wg.Wait()
// Safe to read results after the above synchronization point:
fmt.Println(results)

But if you would try to access the elements of results before wg.Wait(), that's a data race:

// This is data race! Goroutines might still run and modify elements of results!
fmt.Println(results)
wg.Wait()
Giavani answered 17/4, 2018 at 13:29 Comment(0)
B
9

Yes, it's perfectly legal: a slice has an array as its underlying data storage, and, being a compound type, an array is a sequence of "elements" which behave as individual variables with distinct memory locations; modifying them concurrently is fine.

Just be sure to synchronize the shutdown of your worker goroutines with the main one before it reads the updated contents of the slice.

Using sync.WaitGroup for this—as you do—is perfectly fine.

Also, as @icza said, you must not modify the slice value itself (which is a struct containing a pointer to the backing storage array, the capacity and the length).

Bardo answered 17/4, 2018 at 13:32 Comment(1)
Oh, OK, almost word-to-word to the @icza's answer; he beat me to that :-)Bardo
L
2

YES, YOU CAN.

tldr

In golang.org/x/sync/errgroup example, it has the same example code in Example (Parallel)

Google := func(ctx context.Context, query string) ([]Result, error) {
    g, ctx := errgroup.WithContext(ctx)

    searches := []Search{Web, Image, Video}
    results := make([]Result, len(searches))
    for i, search := range searches {
        i, search := i, search

        g.Go(func() error {
            result, err := search(ctx, query)
            if err == nil {
                results[i] = result
            }
            return err
        })
    }
    if err := g.Wait(); err != nil {
        return nil, err
    }
    return results, nil
}
// ...
Limelight answered 18/1, 2021 at 23:4 Comment(0)
R
0

Yes you can, according to Go's memory model. You are writing to separate variables, that map to different regions of memory, so there's no race condition and the writes can resolve consistently.

However, keep in mind that if the slice has small elements and is less than a kB, the elements may be in the same cache line. If several different cores try to write to the same cache line, they will trip over each other doing so and you will pay significant overhead for it at the CPU level:

https://people.freebsd.org/~lstewart/articles/cpumemory.pdf

Though that should not be a problem if the slice is large compared to the size of a cache line, or if the time spent writing to the array is small compared to computing what goes in there.

Rustication answered 11/2, 2024 at 9:40 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.