V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
FreeWong
V2EX  ›  Go 编程语言

=== 一个 golang goroutine 相关的问题 ===

  •  
  •   FreeWong · 144 天前 · 2266 次点击
    这是一个创建于 144 天前的主题,其中的信息可能已经有所发展或是发生改变。
    package main
    
    import (
    	"fmt"
    	"io"
    	"net/http"
    )
    
    func fetch(url string) string {
    	resp, err := http.Get(url)
    	if err != nil {
    		return err.Error()
    	}
    	defer resp.Body.Close()
    	written, err := io.Copy(io.Discard, resp.Body)
    	if err != nil {
    		return err.Error()
    	}
    	result := fmt.Sprintf("%s %s %d", url, resp.Status, written)
    	return result
    }
    
    func request() string {
    	ch := make(chan string)
    	go func() {
    		ch <- fetch("http://www.163.com")
    	}()
    	go func() {
    		ch <- fetch("http://www.sohu.com")
    	}()
    	go func() {
    		ch <- fetch("http://www.sina.com")
    	}()
    	result := <-ch
    	return result
    }
    
    func main() {
    	fmt.Println(request())
    	fmt.Scanln()
    }
    
    

    这里假设 163.com goroutine 总是第一个执行完,此后 request 函数执行返回。 此时,进程未退出,另外两个 goroutine 仍将把 fetch 的结果发送到 ch ,但是由于 ch 是无缓存的,同时又因为 request 已经返回, 无人从 ch 中接收数据,所以另两个 goroutine 应该会死锁,一直无法退出才是。

    但是实际执行时错不报错,这是为什么?多谢

    20 条回复    2024-07-01 16:51:03 +08:00
    povsister
        1
    povsister  
       144 天前   ❤️ 1
    先不吐槽标题了。
    你这个问题很简单,因为卡死的不是“主线程”。
    main 能不能 exit 和其他线程又没关系,只要主线程退出就行,操作系统会负责给你擦屁股的。
    PTLin
        2
    PTLin  
       144 天前
    在 go 的角度,只有这样的代码才算是死锁
    func main() {
    ch:=make(chan int)
    go func () {
    ch<-1
    }()
    ch<-2
    }
    wen20
        3
    wen20  
       144 天前
    ch 是为 goroutine 通信设计, 如果 goroutine 写 ch 报错,那,,,等于把 go 语言脑袋砍成了 o 语言。
    PTLin
        4
    PTLin  
       144 天前
    准确来讲,我理解的 go 中只有所有 goroutine 都因为等待 go 的同步原语( mutex chan 等)而陷入休眠,这时才会运行时报错。
    所以在 go 的角度里,你 main 没有因为等待同步原语休眠,所以没问题。
    例如这段代码,只有 sleep 结束才会运行时报错死锁,因为这时的两个 goroutine 都等待同步原语 chan 而休眠。
    import "time"
    func main() {
    ch:=make(chan int)
    go func () {
    ch<-1
    }()
    go func() {
    time.Sleep(time.Second*10)
    }()
    ch<-2
    }
    mainjzb
        5
    mainjzb  
       144 天前   ❤️ 1
    另两个 goroutine 阻塞了,一直无法退出
    直到整个进程结束
    wxq844688550
        6
    wxq844688550  
       144 天前
    result := <-ch 有一个协程跑完,ch 中就有值了,有了值这里就不会阻塞了,就会执行下面的 return ,然后整个程序就跑完了,根本不会等剩下两个跑完。你这种情况应该使用 sync.waitgroup
    CEBBCAT
        7
    CEBBCAT  
       144 天前
    @wxq844688550 #6 仔细看,有一个 fmt.Scanln() 调用
    yianing
        8
    yianing  
       144 天前
    另外两个 goroutine 是会阻塞,但是不影响 main 协程退出,因为 main 里面没有等待它俩结束
    supuwoerc
        9
    supuwoerc  
       144 天前
    main:死锁的是 goroutine ,关我 main 啥事?!
    wxq844688550
        10
    wxq844688550  
       143 天前
    @CEBBCAT 抱歉,漏看了,理解错了意思。 我理解应该是这样的,ch 是分配在堆上的,虽然 request 执行结束了,但是并没有去关闭 channel ,这部分内存是没有被回收的,协程内部的 ch 依旧是指向那片内存地址的,所以这两个协程实际上是在往正常的阻塞的 ch 中写入,只会阻塞而不会报错
    totoro52
        11
    totoro52  
       143 天前
    什么叫死锁。。 你 result := <-ch 读到了第一个 goroutine 返回来的数据就会直接 return 了,这个函数就退出了,main 执行完两行代码也退出了,主线程退出子协程肯定跟着死,典型儿子像爸爸
    ke1e
        12
    ke1e  
       143 天前 via Android
    这不叫死锁,这叫阻塞。死锁你最起码得有资源竞争,循环等待吧
    guanzhangzhang
        13
    guanzhangzhang  
       143 天前
    你要这样想,如果子协程不退出,主协程就推出不了,那 go 怎么在大型项目里响应处理 ctrl + c
    zzzzaaa
        14
    zzzzaaa  
       143 天前
    产生死锁的条件是什么?
    echoZero
        15
    echoZero  
       143 天前
    golang main 和普通协程一样,并不会去等待其他协程退出,如果需要等待 需要自己实现
    Ipsum
        16
    Ipsum  
       143 天前
    fatal error: all goroutines are asleep - deadlock!

    要所有 goroutines 睡眠才会 deadlock
    zizon
        17
    zizon  
       143 天前
    https://go.dev/play/p/jO_UICpyO9X
    看起来 stdin(也许是其他)是 eof 的.
    sztink
        18
    sztink  
       143 天前
    原因是 main 所在的主协程执行完毕后,会调用 exit_group 系统调用,exit_group 系统调用会 exit 出所有线程,这也就意味着程序的终止。程序都终止了,就不要谈什么阻塞不阻塞了。

    底层实现代码见: https://github.com/cyub/go-1.14.13/blob/master/src/runtime/proc.go#L202-L229 ,代码简单分析下:

    fn := main_main // 是 main 函数的一个 wrapper ,main_main 里面会调用 main 包里面的 main 函数
    fn() // 执行 main_main 函数

    此处省略其他代码...

    exit(0) // 调用系统调用 exit_group ,退出程序
    for {
    var x *int32
    *x = 0 // 由于 x 没有分配内存,此处一定会发生段错误。当然当执行 exit(0)后,理论上也不会进入到这个 for 循环的,这里面可能是为了保险起见吧。
    }

    另外我们可以使用 gdb 捕获 exit_group 系统调用,观察整个过程。怎么捕获系统调用,可以看这个 https://go.cyub.vip/analysis-tools/gdb/#%e4%b8%ba%e7%b3%bb%e7%bb%9f%e8%b0%83%e7%94%a8%e8%ae%be%e7%bd%ae%e6%8d%95%e8%8e%b7%e7%82%b9

    然后执行 gdb 的 bt ,查看 backtrace 信息。
    sztink
        19
    sztink  
       143 天前
    sorry 。漏看了 fmt.Scanln(),搞成程序后面会退出。main 里面的 fmt.Scanln()一直等待内容输入,另外两个 goroutine 会一直阻塞挂起等待 ch 可写入,他两不是死锁。
    sztink
        20
    sztink  
       143 天前
    @sztink 死锁是指多个进程互相等待对方释放资源而无法继续执行的状态,而阻塞等待是指进程由于资源不可用而暂停执行,但资源一旦可用便能继续执行。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3269 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 12:11 · PVG 20:11 · LAX 04:11 · JFK 07:11
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.