DEV Community

林子篆
林子篆

Posted on • Originally published at dannypsnl.github.io on

Create a WaitGroup by yourself

If you had written any concurrency code in Go. I think you could seem sync.WaitGroup before. And today point is to focus on creating a waitgroup by channel trick.

How?

First, you need a channel without the buffer.

func main() {
    wait := make(chan struct{})
}
Enter fullscreen mode Exit fullscreen mode

Then get something from it so if there was no value in wait, the process keeps going. But if just trying to get something from an empty channel. You will get blocking and cause deadlock. Then go will panic it. So we have to change the code a little bit but also be more extendable for next iteration.

// ...
n := 0
wait := make(chan struct{})

for i := 0; i < n; i++ {
    <-wait
}
// ...
Enter fullscreen mode Exit fullscreen mode

Now let’s create works loop.

import (
    "time"
)
// ...
n := 10000
wait := make(chan struct{})

for i := 0; i < n; i++ {
    time.Sleep(time.Microsecond)
    wait <- struct{}{}
}

for i := 0; i < n; i++ {
    <-wait
}
// ...
Enter fullscreen mode Exit fullscreen mode

ps. This code has a little bug(you can try to find it, or read the answer at the end)

Now we can see it work. The reason for this code can work is because size n is our expected amount of workers. After each worker done their jobs. They will send something(here is struct{}{}, but exactly it doesn’t matter thing) into our wait channel. We only read n things from wait.

So after n things are read. We won’t be blocked anymore even wait got the new thing. Else we have to wait wait.

Whole code dependent on this fact. Having this knowledge, we can create ours WaitGroup now.

type waitGroup struct {
    n    int
    wait chan struct{}
}

func WaitGroup() *waitGroup {
    return &waitGroup{
        n:    0,
        wait: make(chan struct{}),
    }
}

func (wg *waitGroup) Add(n int) {
    wg.n = wg.n + n
}

func (wg *waitGroup) Done() {
    wg.wait <- struct{}{}
}

func (wg *waitGroup) Wait() {
    defer close(wg.wait)
    for i := 0; i < wg.n; i++ {
        <-wg.wait
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we use a type wrapping all the thing we need. (It’s a basic idiom, so I don’t want to say why)

Then method Add is preparing for n we talk before. Adding these things in a dynamic way.

Next Done do the thing as we manually do in previous code.

And Wait is read the number of things equal final n.

The end lets say what happened in the previous code. You should close the channel always. So the code will be:

// ...
wait := make(chan interface{})
defer close(wait)
// ...
Enter fullscreen mode Exit fullscreen mode

Maybe you will feel confusing about this part. The reason is channel won’t be collected by GC automatic(if it can, it will be another hell). So always closing it is important.

ps. In productive code, please still using the sync.WaitGroup, I do a test, sync.WaitGroup is 25% faster than the version you see here.

References:

The Go programming language

  • Author: Alan A. A. Donovan & Brian W. Kernighan
  • ISBN: 978-986-476-133-3

Concurrency in Go

  • Author: Katherine Cox-Buday
  • ISBN: 978-1-491-94119-5

Top comments (0)