英文:
Data loss when sending to multiple slices using channel
问题
我最近在学习使用Go语言进行并发编程,我正在考虑编写一个程序,生成一系列的数字并将它们并发地发送到三个切片中。以下是代码:
func main() {
    ch := make(chan int)
    done := make(chan bool)
    var bag1 []int
    var bag2 []int
    var bag3 []int
    go func() {
        for i := 0; i < 1000000; i++ {
            ch <- i
        }
        close(ch)
        done <- true
    }()
    go sendToBag(&bag1, ch)
    go sendToBag(&bag2, ch)
    go sendToBag(&bag3, ch)
    <-done
    len1 := len(bag1)
    len2 := len(bag2)
    len3 := len(bag3)
    fmt.Println("bag1的长度:", len1)
    fmt.Println("bag2的长度:", len2)
    fmt.Println("bag3的长度:", len3)
    fmt.Println("总长度:", len1+len2+len3)
}
func sendToBag(bag *[]int, ch <-chan int) {
    for n := range ch {
        *bag = append(*bag, n)
    }
}
运行结果如下:
bag1的长度:327643
bag2的长度:335630
bag3的长度:336725
总长度:999998
三个切片的总长度并不总是等于发送给它们的数字的数量。所以我的问题是:是什么导致了这个问题,如何改进代码?
英文:
I'm recently learning concurrency in golang, and I'm thinking a program with generate a series of number and then send them to three slices, concurrently. Here's the code:
func main() {
	ch := make(chan int)
	done := make(chan bool)
	var bag1 []int
	var bag2 []int
	var bag3 []int
	go func() {
		for i := 0; i < 1000000; i++ {
			ch <- i
		}
		close(ch)
		done <- true
	}()
	go sendToBag(&bag1, ch)
	go sendToBag(&bag2, ch)
	go sendToBag(&bag3, ch)
	<-done
	len1 := (len(bag1))
	len2 := (len(bag2))
	len3 := (len(bag3))
	fmt.Println("length of bag1:", len1)
	fmt.Println("length of bag2:", len2)
	fmt.Println("length of bag3:", len3)
	fmt.Println("total length:", len1+len2+len3)
}
func sendToBag(bag *[]int, ch <-chan int) {
	for n := range ch {
		*bag = append(*bag, n)
	}
}
which gives the output like the following:
length of bag1: 327643
length of bag2: 335630
length of bag3: 336725
total length: 999998
The total length of three slices doesn't always add up to the count of numbers which were send to them. So my question is: what caused the promblem and how to improve the code
答案1
得分: 5
你对done通道的使用并不能保证所有3个sendToBag goroutine完全完成它们的工作。
虽然在执行<-done之前,for n := range ch {语句确实会将通道“完全排空”,但是你的代码中没有任何保证<-done在最后的*bag = append(*bag, n)之后执行,因此不能保证len()调用仅在append()调用之后执行。
使用等待组(wait group)代替done通道。
func main() {
	ch := make(chan int)
	wg := sync.WaitGroup{}
	var bag1 []int
	var bag2 []int
	var bag3 []int
	go func() {
		for i := 0; i < 1000000; i++ {
			ch <- i
		}
		close(ch)
	}()
	wg.Add(3)
	go sendToBag(&bag1, ch, &wg)
	go sendToBag(&bag2, ch, &wg)
	go sendToBag(&bag3, ch, &wg)
	wg.Wait()
	len1 := len(bag1)
	len2 := len(bag2)
	len3 := len(bag3)
	fmt.Println("bag1的长度:", len1)
	fmt.Println("bag2的长度:", len2)
	fmt.Println("bag3的长度:", len3)
	fmt.Println("总长度:", len1+len2+len3)
}
func sendToBag(bag *[]int, ch <-chan int, wg *sync.WaitGroup) {
	for n := range ch {
		*bag = append(*bag, n)
	}
	wg.Done()
}
链接:https://go.dev/play/p/_wDgS5aS7bI
英文:
Your use of the done channel is simply not enough to guarantee that all 3 sendToBag goroutines finish their job completely.
While it's true that the channel is "fully drained" by the for n := range ch { statement before <-done is executed, there is nothing in your code that ensures that <-done is executed after the final *bag = append(*bag, n) is executed, and therefore there is no guarantee that the len() calls on the bags will be executed only after the append() calls.
Use a wait group instead of the done channel.
func main() {
	ch := make(chan int)
	wg := sync.WaitGroup{}
	var bag1 []int
	var bag2 []int
	var bag3 []int
	go func() {
		for i := 0; i < 1000000; i++ {
			ch <- i
		}
		close(ch)
	}()
	wg.Add(3)
	go sendToBag(&bag1, ch, &wg)
	go sendToBag(&bag2, ch, &wg)
	go sendToBag(&bag3, ch, &wg)
	wg.Wait()
	len1 := (len(bag1))
	len2 := (len(bag2))
	len3 := (len(bag3))
	fmt.Println("length of bag1:", len1)
	fmt.Println("length of bag2:", len2)
	fmt.Println("length of bag3:", len3)
	fmt.Println("total length:", len1+len2+len3)
}
func sendToBag(bag *[]int, ch <-chan int, wg *sync.WaitGroup) {
	for n := range ch {
		*bag = append(*bag, n)
	}
	wg.Done()
}
答案2
得分: 0
@mkopriva的答案是首选的,但如果你只想使用通道,这里是解决方案。
func main() {
	ch := make(chan int)
	done := make(chan bool)
	var bag1 []int
	var bag2 []int
	var bag3 []int
	go func() {
		for i := 0; i < 1000000; i++ {
			ch <- i
		}
		close(ch)
		done <- true
	}()
	ch1 := make(chan bool)
	ch2 := make(chan bool)
	ch3 := make(chan bool)
	go sendToBag(&bag1, ch, ch1)
	go sendToBag(&bag2, ch, ch2)
	go sendToBag(&bag3, ch, ch3)
	<-ch1
	<-ch2
	<-ch3
	<-done
	len1 := len(bag1)
	len2 := len(bag2)
	len3 := len(bag3)
	fmt.Println("bag1的长度:", len1)
	fmt.Println("bag2的长度:", len2)
	fmt.Println("bag3的长度:", len3)
	fmt.Println("总长度:", len1+len2+len3)
}
func sendToBag(bag *[]int, ch <-chan int, ch1 chan bool) {
	for n := range ch {
		*bag = append(*bag, n)
	}
	ch1 <- true
}
在sendToBag函数中添加了更多的通道,因此主goroutine将被阻塞,直到其他goroutine在ch1/ch2/ch3上写入数据,这样可以确保在执行最后的*bag = append(*bag, n)之后执行<-done。
英文:
@mkopriva's answer is preferred but if you want to do with channel only, here is the solution.
func main() {
	ch := make(chan int)
	done := make(chan bool)
	var bag1 []int
	var bag2 []int
	var bag3 []int
	go func() {
		for i := 0; i < 1000000; i++ {
			ch <- i
		}
		close(ch)
		done <- true
	}()
	ch1 := make(chan bool)
	ch2 := make(chan bool)
	ch3 := make(chan bool)
	go sendToBag(&bag1, ch, ch1)
	go sendToBag(&bag2, ch, ch2)
	go sendToBag(&bag3, ch, ch3)
	<-ch1
	<-ch2
	<-ch3
	<-done
	len1 := (len(bag1))
	len2 := (len(bag2))
	len3 := (len(bag3))
	fmt.Println("length of bag1:", len1)
	fmt.Println("length of bag2:", len2)
	fmt.Println("length of bag3:", len3)
	fmt.Println("total length:", len1+len2+len3)
}
func sendToBag(bag *[]int, ch <-chan int, ch1 chan bool) {
	for n := range ch {
		*bag = append(*bag, n)
	}
	ch1 <- true
}
https://go.dev/play/p/id3l_VtUq8E
Here we add more channels in the sendToBag function, so the main goroutine will be blocked until some other goroutine write on ch1/ch2/ch3, which ensures <-done is executed after the final *bag = append(*bag, n) is executed.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论