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

156 天前
 FreeWong
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 应该会死锁,一直无法退出才是。

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

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

要所有 goroutines 睡眠才会 deadlock
zizon
155 天前
https://go.dev/play/p/jO_UICpyO9X
看起来 stdin(也许是其他)是 eof 的.
sztink
155 天前
原因是 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
155 天前
sorry 。漏看了 fmt.Scanln(),搞成程序后面会退出。main 里面的 fmt.Scanln()一直等待内容输入,另外两个 goroutine 会一直阻塞挂起等待 ch 可写入,他两不是死锁。
sztink
155 天前
@sztink 死锁是指多个进程互相等待对方释放资源而无法继续执行的状态,而阻塞等待是指进程由于资源不可用而暂停执行,但资源一旦可用便能继续执行。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1053696

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX