难以理解电子书中的并发示例

I'm having difficulty understanding concurrency from an ebook. My hopes are someone can do a breakdown step by step with me so i can clearly understand whats going on.

Here is the main method:

func main(){
  c := make(chan int)
  go printer(c)
  wg.Add(1)

  // Send 10 integers on the channel.
  for i := 1; i <= 10; i++ {
    c <- i
  }

  close(c)
  wg.Wait()
}

And here is the printer method:

func printer(ch chan int) {
    for i := range ch {
        fmt.Printf("Received %d ", i)
    }
    wg.Done()
}

Here are my questions:

  • Why are we only executing wg.Add(1) as one group to wait for instead of wg.Add(1) inside the main method for loop
  • I'm really not understanding channels, period.

I've done some research and no one seems to be able to explain it to me in a simple way.

Any step by step simple explanation as to sending integers on channel and adding to wait groups would be appreciated.

UPDATE

sourcecode => https://github.com/goinaction/code/blob/master/chapter1/channels/hellochannels.go

Editor's note: this first paragraph was in response to the author's original code, which has since been edited to have markedly different semantics than the code that was originally presented.

The code example you've provided is not a particularly good example as far as demonstrating the usage of a WaitGroup, because the program is terminated by the end of the main method rather than any co-ordination handled by the WaitGroup. You can remove the call to wg.Done() and that program will still compile and execute fine.

I'm happy to answer your more general questions, though.

Why are we only executing wg.Add(1) as one group to wait for instead of wg.Add(1) inside the main method for loop?

It sounds like you're asking why we don't increment wg for each integer in the for loop. This is not really the point of WaitGroups. A typical use case would be adding 1 for each goroutine that we're co-ordinating beyond the main goroutine. In this example, there's one other goroutine (the one handling printer, so we only add 1 to our WaitGroup.

A more clear version of your above example would be something like the following:

var wg sync.WaitGroup

func printer(ch chan int) {
    for i := range ch {
        fmt.Printf("Received %d ", i)
    }
    wg.Done()
}

func sender(ch chan int) {
    for i := 0; i <= 10; i++ {
        ch <- i
    }
    close(ch)
}

func main() {
    c := make(chan int)
    go printer(c)
    go sender(c)
    wg.Add(1)

    fmt.Println("Waiting...")
    wg.Wait()
    fmt.Println("Finished")
}

In this example, we want to wait for the printer method to have fully printed off all of its numbers we proceed with the main goroutine. So we tell our main goroutine to Wait() until all integers have been printed, at which point we tell the main thread to proceed. Unlike your example, we close the channel once we've entered all of our ints into it, which allows the printer function to stop blocking on range and to go ahead and call wg.Done - in your original example, the printer never stops blocking on range and so never actually calls wg.Done

I'm really not understanding channels, period.

Hopefully the above example makes things a little more clear? I'm afraid this part of your question is a bit too broad for me to answer.

Unless you deliberately add a blocking point in the main thread, the program will seize execution before the last item is printed by the receiver. That is the job that is been done by the WaitGroup object and the WaitGroup.Wait() call.

With this you are telling the main thread that another operation is concurrently occurring and that you want to wait for it's completion before ending the execution of your program. That will be signaled with the wg.Done() statement.

However in this case this is only needed because otherwise the receiver would not be able to receive the last item, before the execution on the main thread concluded. But for the most part, the channels are also a synchronization abstraction, and they are dictating the flow of the execution of the program.

But first things first, the reason why the WaitGroup is not being added in the loop, is because you only have a single point in your program which you obligatory want to wait for. A single goroutine, which is composed of a single channel.

Since in the example, we are working with an Unbuffered channel, it only allows 1 message at a time to be transmitted through the channel. This means that the for each message the transmitter send, the transmitter will be blocked until such message is retrieved by the receiver and removed from the channel.

When the transmitter sends the last item , it will close the channel indicating to the receiver that there will not be more messages to be received later on. The iteration occurring with the range ch will terminate after receiving the "10", and the execution of the goroutine finally breaks off the loop execution.

This is what was happening:

// Transmitter sends "1"

// Transmitter is now blocked since the channel is unbuffered.

// Receiver receives "1"

// Receiver is now blocked until something is transmitted to the channel.

// Transmitter is now unblocked and can transmit again.

// .....

// Transmitter sends "10"

// Receiver received "10"

After this, the go function can finally subtract the counter on the WaitGroup, by calling it's static method Done, and the counter will reach zero, meaning that the block operation occurring due the wg.Wait() will seize to exist, and the main thread can finally reach completion and terminate the program execution.

Key points:

  • The channel which we are utilizing its a unbuffered channel, meaning 1 message at a time. With buffered channels on the other hand, you could have multiple messages at the same time being transmitted, before the receiver actually decides to go get them, and the advantages of the WaitGroup could be become a lot more obvious.
  • with <- i, you are sending something to the channel, and with range ch you are retrieved such messages. The messages transmitted are of a fixed variable type, in this case integer.