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