Golang中关于通道的问题探讨

Golang中关于通道的问题探讨 大家好,

希望这不是一个愚蠢的问题。我已经在这个论坛里搜索过,但没有找到完全符合我需求的内容。这篇关于可能存在的竞态条件的帖子似乎很接近。

我一直在golangdocs.com上查看关于通道的内容,学习通过通道发送自定义数据。我做了一个微小的改动,在被调用的函数中添加了一个Printf语句。我会把整个代码粘贴在这里,以便于查看。

package main

import (
	"fmt"
)

type Person struct {
	Name string
	Age  int
}

func SendPerson(ch chan Person, p Person) {
	ch <- p
	fmt.Printf("SendPerson %v\n", p)
}

func main() {

	p := Person{"John", 23}

	ch := make(chan Person)

	go SendPerson(ch, p)

	name := (<-ch).Name
	fmt.Println(name)
}

如果我多次运行这段代码,通常只看到“John”。但有时会看到:

SendPerson {John 23}
John

我不太明白这是为什么。起初我以为这是我的错误,因为我应该把Printf语句放在被调用函数中处理通道的部分之前,因为我想看看我接收到了什么(这也是我最初添加Printf的目的)。但后来我又仔细想了想。为什么我有时会看到这个输出?我原本期望要么总是看到,要么永远看不到,而不是有时看似随机地出现。我不确定这是否与通道是缓冲的还是非缓冲的有关。

有人能为我指明正确的方向吗?

非常感谢。


更多关于Golang中关于通道的问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

9 回复

gorountine runtime is uncertain.

啊哈。我想这已经回答了问题。

更多关于Golang中关于通道的问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你好 @GonzaSaya

谢谢,我猜是竞态条件。但我当时不太明白原因。

再次感谢。 John。

goroutine 的运行时是不确定的。 尝试这样做,它不会阻塞直到有消息被发送到通道

for p:=range ch{
fmt.Println(p.name)
}

SendMessage 函数(及其协程)在能够打印消息之前就结束了。 尝试先打印,然后再将消息发送到通道。

当主函数结束时,它会终止所有其他协程。 这类似于一种竞态条件。

我明白了,是的,当你运行 fmt.Print 等函数时,并不能保证它会被执行,因为 goroutine 的执行顺序是没有保证的。此外,当你的主 goroutine 结束时,其他 goroutine 也会随之结束,这可能导致你的打印信息永远不会出现。

非常感谢您提供的代码。它看起来确实比最初的示例要复杂得多。

我断断续续地使用 Go 语言编程已经有一段时间了,但还没有真正使用过通道。在我第一次接触它们时,就遇到了某种竞态条件,或者至少是意料之外的行为,这让我有点惊讶。

我并没有完全理解你的问题。它打印出“John”是因为你只取了那部分内容:

name := (<-ch).Name
fmt.Println(name)

所以,如果你想要通道中的整个结构体,只需:

perso := <-ch fmt.Println(perso)

但就像我之前说的,我不确定我是否理解了你的问题。请告诉我…

你可以改变向通道发送消息的方法,因为你能保证消息被记录。当然,前提是你的程序在后台运行。如果你只能运行一下就结束,那么你可以阻塞等待消息进入通道,或者使用 sync.WaitGroup。

package main

import (
	"fmt"
)

type Person struct {
	Name string
	Age  int
	Message chan string
}

func NewPerson(name string, age int) *Person {
	return &Person{
		Name: name,
		Age: age,
		Message: make(chan string),
	}
}

func (s *Person) Send(ch chan *Person) {
	s.Message <- "The message was sent"

	ch <- s
}

func (s *Person) ReadMessage() {
	go func(){
		for m := range s.Message {
			fmt.Println(m)
		}
	}()
}

func main() {
	p := NewPerson("John", 23)
	p. ReadMessage()

	ch := make(chan *Person)
	go p.Send(ch)

	fmt.Println((<-ch).Name)
}

https://play.golang.com/p/D4akv-iVII__i

这是一个关于Golang通道和goroutine调度顺序的典型问题。你的观察是正确的,这与通道的缓冲特性以及goroutine的调度机制有关。

在你的代码中,通道是非缓冲的(make(chan Person)),这意味着发送和接收操作是同步的。当SendPerson执行ch <- p时,它会阻塞直到main函数执行<-ch。然而,这两个goroutine(mainSendPerson)的执行顺序并不确定。

关键点:

  • fmt.Printf("SendPerson %v\n", p)ch <- p之后执行
  • fmt.Println(name)(<-ch).Name之后执行
  • 这两个打印语句在不同的goroutine中,执行顺序取决于调度器

示例重现:

package main

import (
	"fmt"
	"time"
)

type Person struct {
	Name string
	Age  int
}

func SendPerson(ch chan Person, p Person) {
	ch <- p
	fmt.Printf("SendPerson %v\n", p)
}

func main() {
	// 多次运行观察不同结果
	for i := 0; i < 10; i++ {
		p := Person{"John", 23}
		ch := make(chan Person)
		
		go SendPerson(ch, p)
		
		name := (<-ch).Name
		fmt.Println(name)
		
		// 添加短暂延迟,让调度更明显
		time.Sleep(time.Microsecond)
	}
}

输出可能类似:

John
SendPerson {John 23}
John
SendPerson {John 23}
SendPerson {John 23}
John
John
SendPerson {John 23}

原因分析:

  1. main接收数据后,两个goroutine都解除阻塞
  2. SendPerson中的Printfmain中的Println开始竞争执行
  3. 如果SendPerson先被调度,你会看到SendPerson先打印
  4. 如果main先被调度,你会看到John先打印
  5. Go调度器的不确定性导致了这种随机性

验证方案:

// 使用WaitGroup确保顺序观察
package main

import (
	"fmt"
	"sync"
)

type Person struct {
	Name string
	Age  int
}

func SendPerson(ch chan Person, p Person, wg *sync.WaitGroup) {
	defer wg.Done()
	ch <- p
	fmt.Printf("SendPerson %v\n", p)
}

func main() {
	var wg sync.WaitGroup
	
	for i := 0; i < 5; i++ {
		wg.Add(1)
		p := Person{fmt.Sprintf("John%d", i), 23 + i}
		ch := make(chan Person)
		
		go SendPerson(ch, p, &wg)
		
		name := (<-ch).Name
		fmt.Printf("Received: %s\n", name)
	}
	
	wg.Wait()
}

要确保总是先看到"SendPerson"打印,需要在发送前打印:

func SendPerson(ch chan Person, p Person) {
	fmt.Printf("SendPerson %v\n", p)  // 先打印
	ch <- p                           // 后发送
}

或者使用缓冲通道来解耦发送和接收:

func main() {
	p := Person{"John", 23}
	ch := make(chan Person, 1)  // 缓冲大小为1
	
	go SendPerson(ch, p)
	
	// 发送不会阻塞,SendPerson可以立即继续执行Printf
	time.Sleep(time.Millisecond)  // 确保SendPerson有机会执行
	
	name := (<-ch).Name
	fmt.Println(name)
}

这种不确定性是Go并发模型的设计特性,不是代码错误。

回到顶部