Golang中循环与WaitGroup等待并从通道读取数据的比较

Golang中循环与WaitGroup等待并从通道读取数据的比较 我在想,为了确保主协程在 go 函数返回之前不会返回,使用 Waitgroup 而不是循环是否会让这段代码读起来更好。我正在尝试通过模拟从网站获取标题并检查标题是否存在来练习通道的管道操作。

具体来说,在下面的代码中,我无论如何都会循环 5 次来从通道读取,那么使用 Waitgroup 有什么用呢?我是否应该先等待,然后遍历管道中的最后一个通道?

package main

import (
	"fmt"
	"time"
)

// 模拟从网站获取一些数据。
func getTitle(titlesChan chan<- string) {
	time.Sleep(1 * time.Second)
	titlesChan <- "Google"
}

func hasTitle(titlesChan <-chan string, hasChan chan<- bool) {
	time.Sleep(1 * time.Second)
	titles := <-titlesChan
	hasChan <- titles != ""
}

func main() {
	titleChan := make(chan string)
	hasTitleChan := make(chan bool)
       
        // 假设我获取标题并检查是否存在,执行 5 次。
	for x := 1; x < 5; x++ {
		go getTitle(titleChan)
		go hasTitle(titleChan, hasTitleChan)
	}
	for x := 1; x < 5; x++ {
		fmt.Println(<-hasTitleChan)
		if x == 4 {
			close(hasTitleChan)
		}
	}

}

https://play.golang.org/p/MuIe_GrEWtY


更多关于Golang中循环与WaitGroup等待并从通道读取数据的比较的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

你好 @kahunacohen,你能提供更多背景信息吗? 为什么你会同时获取到5次标题? 你能使用一个带有重试机制且在重试之间有延迟的REST客户端吗? 类似这样:

func main() {
    fmt.Println("hello world")
}

更多关于Golang中循环与WaitGroup等待并从通道读取数据的比较的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


抱歉,这确实只是一个模拟。我正在尝试理解管道通道。这里的5次是随意设定的。我在模拟API调用、解析等操作可能的样子。比如说,假设我正在遍历一个包含5个URL的数组……我真正的问题是使用循环来从通道读取数据并关闭通道,这样主函数就不会在goroutine执行完毕前退出。有没有使用 WaitGroup 的更好方法?我能理解在你不需要从通道获取数据时的使用场景,但无论如何你还是得在循环中读取数据,对吧?

在这种情况下,我认为你不需要使用 WaitGroup。 你可以将关闭函数移到循环外部。 类似这样:

package main

import (
	"fmt"
)

type titleResult struct {
	title   string
	results bool
}

func getTitle(titlesIn <-chan string, titleResultOutput chan<- titleResult) {
	for x := range titlesIn {
		// 获取标题 X 的信息
		titleResultOutput <- titleResult{title: x, results: true}
	}
}

func main() {
	workersCount := 5

	// 要获取的标题
	titleNames := []string{"google", "saya", "ml", "google_2", "saya_2", "ml_2"}

	titles := make(chan string, len(titleNames))
	titlesResults := make(chan titleResult, len(titleNames))

	// 创建 N 个工作协程
	for x := 1; x <= workersCount; x++ {
		go getTitle(titles, titlesResults)
	}

	// 向工作协程发送标题
	go func() {
		for _, title := range titleNames {
			titles <- title
		}
		close(titles)
	}()

	// 处理结果
	for x := 1; x <= len(titleNames); x++ {
		getResult := <-titlesResults // 这一行会阻塞执行,直到工作协程处理完标题
		fmt.Printf("%d - %+v\n", x, getResult)
	}
	close(titlesResults)
}

在并发编程中,WaitGroup 和通道读取循环是两种不同的同步模式,适用于不同的场景。你的代码存在数据竞争问题,因为多个 getTitle 协程向同一个无缓冲通道写入,而多个 hasTitle 协程从同一个通道读取,这会导致不可预测的行为。

以下是使用 WaitGroup 改进的版本,它更清晰地管理协程生命周期:

package main

import (
	"fmt"
	"sync"
	"time"
)

func getTitle(titleChan chan<- string, wg *sync.WaitGroup) {
	defer wg.Done()
	time.Sleep(1 * time.Second)
	titleChan <- "Google"
}

func hasTitle(titleChan <-chan string, resultChan chan<- bool, wg *sync.WaitGroup) {
	defer wg.Done()
	time.Sleep(1 * time.Second)
	title := <-titleChan
	resultChan <- (title != "")
}

func main() {
	const workerCount = 5
	titleChan := make(chan string, workerCount)
	resultChan := make(chan bool, workerCount)
	var wg sync.WaitGroup

	// 启动生产者协程
	wg.Add(workerCount)
	for i := 0; i < workerCount; i++ {
		go getTitle(titleChan, &wg)
	}

	// 启动消费者协程
	wg.Add(workerCount)
	for i := 0; i < workerCount; i++ {
		go hasTitle(titleChan, resultChan, &wg)
	}

	// 等待所有协程完成
	wg.Wait()
	close(resultChan)

	// 读取结果
	for result := range resultChan {
		fmt.Println(result)
	}
}

关键改进:

  1. 使用带缓冲的通道避免死锁
  2. 每个协程配对独立的通道通信,避免数据竞争
  3. WaitGroup 确保所有协程完成后再关闭结果通道
  4. 使用 range 循环安全读取关闭后的通道

如果不需要中间结果聚合,更简洁的版本是:

func main() {
	const workerCount = 5
	var wg sync.WaitGroup

	for i := 0; i < workerCount; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			titleChan := make(chan string, 1)
			resultChan := make(chan bool, 1)

			go func() {
				time.Sleep(1 * time.Second)
				titleChan <- "Google"
			}()

			go func() {
				time.Sleep(1 * time.Second)
				title := <-titleChan
				resultChan <- (title != "")
			}()

			fmt.Println(<-resultChan)
		}(i)
	}
	wg.Wait()
}

选择依据:

  • 需要收集所有结果时:使用 WaitGroup + 缓冲通道
  • 只需执行并发任务时:直接使用 WaitGroup 等待
  • 需要流水线处理时:使用通道链式传递数据
回到顶部