使用For循环的Goroutines和缓冲通道

huangapple go评论89阅读模式
英文:

Buffered Channels with Goroutines using For loop

问题

我是一个golang的新手,正在尝试使用goroutines实验带缓冲通道。我以为我理解了如何使用goroutines和带缓冲通道,直到我遇到了下面的例子,这对我来说成了一个脑筋急转弯,打破了我迄今为止学到的概念。

这是我从文章https://medium.com/rungo/anatomy-of-channels-in-go-concurrency-in-go-1ec336086adb中摘取的原始例子。

代码#1:(通道容量=3,通道长度=3,循环长度=4)

func squares(c chan int) {
	for i := 0; i <= 3; i++ {
		num := <-c
		fmt.Println(num * num)
	}
}

func main() {
	fmt.Println("main() started")
	c := make(chan int, 3)

	go squares(c)

	c <- 1
	c <- 2
	c <- 3
	
	fmt.Println("main() stopped")
}

输出:

main() started
main() stopped

解释: 在上面的程序中,通道c的缓冲容量为3。这意味着它可以容纳3个值。由于缓冲区没有溢出(因为我们没有推送任何新值),主goroutine不会阻塞,程序会退出。
我理解了这个例子。

代码#2:(通道容量=3,通道长度=4,循环长度=4)

func squares(c chan int) {
	for i := 0; i <= 3; i++ {
		num := <-c
		fmt.Println(num * num)
	}
}

func main() {
	fmt.Println("main() started")
	c := make(chan int, 3)

	go squares(c)

	c <- 1
	c <- 2
	c <- 3
	c <- 4 // goroutine在这里阻塞
	
	fmt.Println("main() stopped")
}

输出:

main() started
1
4
9
16
main() stopped

解释: 现在填满的缓冲区通过c <- 4发送操作得到推送,主goroutine被阻塞,而squares goroutine则将所有值都读取出来。
我也理解了这个例子。

代码#3:(通道容量=3,通道长度=5,循环长度=5)

func squares(c chan int) {
	for i := 0; i <= 4; i++ {
		num := <-c
		fmt.Println(num * num)
	}
}

func main() {
	fmt.Println("main() started")
	c := make(chan int, 3)

	go squares(c)

	c <- 1
	c <- 2
	c <- 3
	c <- 4 // goroutine在这里阻塞
	c <- 5

	fmt.Println("main() stopped")
}

输出:

main() started
1
4
9
16
25
main() stopped

解释: 我向通道中添加了另一个值,即5。尽管通道容量只有3。

我理解,直到通道接收到n+1个发送操作,它才会阻塞当前的goroutine。在值为4时,它接收到了n+1个操作,这就是为什么goroutine被阻塞并且读取了所有的值。但我无法理解的是,通道如何处理n+2个操作。是因为我们已经从通道中读取了值,并且还有更多的空间可以读取吗?

英文:

I am a newbie in golang and trying to experiment buffered channels with goroutines. I thought I understood how buffered channels work with goroutines until encountered the below example which becomes a brain teaser for me and gave a bang to the concepts that I have learned so far.

This is the original example that I took from the article https://medium.com/rungo/anatomy-of-channels-in-go-concurrency-in-go-1ec336086adb.

Code#1: (channel capacity=3, channel length=3, loop length=4)

func squares(c chan int) {
	for i := 0; i &lt;= 3; i++ {
		num := &lt;-c
		fmt.Println(num * num)
	}
}

func main() {
	fmt.Println(&quot;main() started&quot;)
	c := make(chan int, 3)

	go squares(c)

	c &lt;- 1
	c &lt;- 2
	c &lt;- 3
	
	fmt.Println(&quot;main() stopped&quot;)
}

Output:

main() started
main() stopped

Explanation: In the above program, channel c has a buffer capacity of 3. That means it can hold 3 values. Since the buffer is not overflowing (as we didn’t push any new value), the main goroutine will not block and the program exists.
I have understood this example.

Code#2: (channel capacity=3, channel length=4, loop length=4)

func squares(c chan int) {
	for i := 0; i &lt;= 3; i++ {
		num := &lt;-c
		fmt.Println(num * num)
	}
}

func main() {
	fmt.Println(&quot;main() started&quot;)
	c := make(chan int, 3)

	go squares(c)

	c &lt;- 1
	c &lt;- 2
	c &lt;- 3
	c &lt;- 4 // goroutine blocks here
	
	fmt.Println(&quot;main() stopped&quot;)
}

Output:

main() started
1
4
9
16
main() stopped

Explanation: As now a filled buffer gets the push by c <- 4 send operation, main goroutine blocks and squares goroutine drains out all the values.
It is also understood by me.

Code#3: (channel capacity=3, channel length=5, loop length=5)

func squares(c chan int) {
	for i := 0; i &lt;= 4; i++ {
		num := &lt;-c
		fmt.Println(num * num)
	}
}

func main() {
	fmt.Println(&quot;main() started&quot;)
	c := make(chan int, 3)

	go squares(c)

	c &lt;- 1
	c &lt;- 2
	c &lt;- 3
	c &lt;- 4 // goroutine blocks here
	c &lt;- 5

	fmt.Println(&quot;main() stopped&quot;)
}

Output:

main() started
1
4
9
16
25
main() stopped

Explanation: I have added another value to the channel which is 5. Although the channel capacity is only 3.

I understand that until the channel receives n+1 send operations, it won’t block the current goroutine. On the value 4, it receives n+1 operations, that's why goroutine gets blocked and drains out all the values but what I am unable to understand is that how n+2 operations are dealt by channel. Is it because we have read the values from the channel and we have more space for reading?

答案1

得分: 4

通道容量在这里没有被充满,因为你的squares goroutine正在运行,并且它立即接收到被发送到通道的值。

但我无法理解的是,通道是如何处理n+2个操作的。

在第n+1个发送操作时,通道容量已满,因此它会阻塞。在至少接收到一个值(因此有空间可以发送下一个值)后,第n+1个发送操作继续进行,容量再次变满。现在,在第n+2个发送操作时,由于容量已满,它将阻塞,直到至少接收到一个值为止,依此类推。

英文:

The channel capacity is not getting full here because your squares goroutine is running and it immediately receives values that are being sent to the channel.

> but what I am unable to understand is that how n+2 operations are
> dealt by channel.

At n+1 send operation, channel capacity is full so it will block. After at least one value is received from the channel (so a space is available to send next value) n+1 send operation continues and again capacity is full. Now at n+2 send operation, since the capacity is full so it will block until at least one value is received from channel and so on.

答案2

得分: 0

你观察到某种方式调度程序对你的程序的操作进行排序,但是你展示的代码并不能保证你的指令总是以这种方式执行。

你可以尝试运行你的程序100次,看看是否总是有相同的输出:

go build myprogram
for i in {1..100}; do
  ./myprogram
done

你还可以打开竞争检测器(竞争检测器的一个效果是在调度程序上引入更多的随机性):

go build -race myprogram
for i in {1..100}; do
  ./myprogram
done

以下是一些输出,也与你最后的“5个项目”示例兼容:

main() started
1
4
9
16
main() stopped
main() started
1
4
main() stopped
9
main() started
1
main() stopped

为了更具体地说明如何实现“始终具有相同行为”,以下是在Go中常用的几种方法,可以使你的示例程序在退出之前运行所有任务:

  • 使用sync.WaitGroup,使squares()函数指示其已完成工作:
func squares(c chan int, wg *sync.WaitGroup) {
    defer wg.Done()  // &lt;- 从该函数返回时将计数器减1

    for i := 0; i &lt;= 3; i++ {
        num := &lt;-c
        fmt.Println(num * num)
    }
}

func main() {
    fmt.Println(&quot;main() started&quot;)
    c := make(chan int, 3)
    var wg sync.WaitGroup
    
    wg.Add(1)   // &lt;- 增加等待组计数器
    go squares(c, &amp;wg)

    c &lt;- 1
    c &lt;- 2
    c &lt;- 3
    c &lt;- 4
   
    wg.Wait() // &lt;- 等待计数器返回0
 
    fmt.Println(&quot;main() stopped&quot;)
}
  • main()函数中:在完成向通道提供值后关闭通道,
    squares函数中:使用range遍历通道以获取所有要处理的值
// 告诉编译器“该通道只会用作接收器”
func squares(c &lt;-chan int, wg *sync.WaitGroup) {
    defer wg.Done()

    // 在通道关闭之前消耗所有的值:
    for num := range c {
        fmt.Println(num * num)
    }
}

func main() {
   ...

   c &lt;- 1
   c &lt;- 2
   c &lt;- 3
   c &lt;- 4
   c &lt;- 5
   c &lt;- 6
   c &lt;- 7
   close(c)  // 关闭通道以表示“没有更多的值”

   wg.Wait()

   ...
}

通过以上修改,你的程序将始终在退出之前将所有值打印到标准输出。

playground:https://play.golang.org/p/qD_FHCpiub7

英文:

You observe a certain way the scheduler orders the actions of your porgram, but the code you displayed does not guarantee that your instructions will always be executed this way.

You can try to run your program 100 times, and see if you always have the same output :

go build myprogram
for i in {1..100}; do
  ./myprogram
done

You can also turn the race detector on (one effect of the the race detector is that it introduces more randomization on the scheduler) :

go build -race myprogram
for i in {1..100}; do
  ./myprogram
done

Here are some outputs which would also be compatible with your last "5 items" example :

main() started
1
4
9
16
main() stopped
main() started
1
4
main() stopped
9
main() started
1
main() stopped

To give a more concrete view of what can be made to have "always the same behavior", here pretty standard ways in go to have your sample program run all its tasks before exiting :

  • use a sync.WaitGroup, to have the squares() function indicate it has completed its work :
func squares(c chan int, wg *sync.WaitGroup) {
    defer wg.Done()  // &lt;- decrement the counter by 1 when
                     //    returning from this function

    for i := 0; i &lt;= 3; i++ {
        num := &lt;-c
        fmt.Println(num * num)
    }
}

func main() {
    fmt.Println(&quot;main() started&quot;)
    c := make(chan int, 3)
    var wg sync.WaitGroup
    
    wg.Add(1)   // &lt;- increment the waitgroup counter
    go squares(c, &amp;wg)

    c &lt;- 1
    c &lt;- 2
    c &lt;- 3
    c &lt;- 4
   
    wg.Wait() // &lt;- wait for the counter to go back to 0
 
    fmt.Println(&quot;main() stopped&quot;)
}
  • in main() : close the channel when you are done feeding values,
    in squares : use range over the channel to get all the values to process
// you can tell the compiler &quot;this channel will be only used as a receiver&quot;
func squares(c &lt;-chan int, wg *sync.WaitGroup) {
    defer wg.Done()

    // consume all values until the channel is closed :
    for num := range c {
        fmt.Println(num * num)
    }
}

func main() {
   ...

   c &lt;- 1
   c &lt;- 2
   c &lt;- 3
   c &lt;- 4
   c &lt;- 5
   c &lt;- 6
   c &lt;- 7
   close(c)  // close the channel to signal &quot;no more values&quot;

   wg.Wait()

   ...
}

With the above modifications, your program will always print all of its values on stdout before exiting.

playground : https://play.golang.org/p/qD_FHCpiub7

huangapple
  • 本文由 发表于 2021年7月19日 16:50:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/68437646.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定