Concurrency in Go (Part 2)

I made an article on concurrency in GoLang a year ago, Concurrency in Go (Part 1). I put "Part 1" on the title which caused people to wait for the second part. I also almost forgot to create the second part. Here we go.

There is a problem with concurrency. Functions that process data will have problems when running in multiple Goroutines. It happens because the data is shared and executed concurrently by multiple Goroutines. There is a conflict. It is called a race condition. A condition in which the behavior of the system depends on the sequence of events. We can solve it by using only a Goroutine or mutual exclusion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "log"

type Counter struct {
    v int
}

func (c *Counter) Increment() {
    c.v++
}

func (c *Counter) Value() int {
    return c.v
}

func main() {
    c := &Counter{}
    for i := 0; i < 1000; i++ {
        c.Increment()
    }

    log.Println(c.Value())
}

Let's execute the code above. We can see that there is "1000" in our terminal.

1
2
3
4
5
6
7
8
9
10
package main

import "testing"

func Benchmark(b *testing.B) {
    c := &Counter{}
    for i := 0; i < 1000; i++ {
        c.Increment()
    }
}

You can try to benchmark that linear function to measure execution time on your computer, go test --bench=.. Most computers only need less than 1 second.

The v is the data that we want to process. We increment it. In any case, the data could be anything. Let's put the incrementation on the Goroutine for an example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
    "log"
    "sync"
)

type Counter struct {
    v  int
    wg *sync.WaitGroup
}

func (c *Counter) Increment() {
    c.v++
    c.wg.Done()
}

func (c *Counter) Value() int {
    return c.v
}

func main() {
    var wg sync.WaitGroup
    c := &Counter{wg: &wg}
    for i := 0; i < 1000; i++ {
        c.wg.Add(1)
        go c.Increment()
    }

    c.wg.Wait()
    log.Println(c.Value())
}

We use WaitGroup to wait for a group of Goroutines to finish the execution. We add 1 to the WaitGroup and decrement it on the Increment function. It executes the next step when it turns 0.

You can try it on your local machine multiple times. It would be a random number and never be 1000. As I mentioned above, it is a race condition.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
    "log"
    "sync"
    "time"
)

type Counter struct {
    m sync.Mutex
    v int
}

func (c *Counter) Increment() {
    c.m.Lock()
    defer c.m.Unlock()

    c.v++
}

func (c *Counter) Value() int {
    c.m.Lock()
    defer c.m.Unlock()

    return c.v
}

func main() {
    c := &Counter{}
    for i := 0; i < 1000; i++ {
        go c.Increment()
    }

    time.Sleep(time.Second)
    log.Println(c.Value())
}

We solve it by using the mutual exclusion method, or Mutex in GoLang. You can try to run the code above. We only need a second to wait for the Goroutine to finish. The result will be always 1000.

References

Related Articles