前一篇文章说到 http.Client 巧妙利用 chan 进行协程间通信和超时控制,其实这里面有很多细节,包括需要注意避免协程泄露等。
由于 http.Client 源码太长,我就简单模拟了一下协程通信和超时处理的场景。
问题代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
package main
import "time"
func main() {
// 这里是同步等待执行完毕,deal 有超时控制
result := deal()
print("执行完毕,result 为:", result, "\n")
time.Sleep(time.Second * 5)
print("父协程结束\n")
}
func deal() string {
timeout := time.NewTimer(time.Second * 2)
// 结束后立刻关闭计时器,防止高并发情况下很多个计时器运行,造成性能损耗
defer timeout.Stop()
done := make(chan string)
result := ""
// 开启一个协程异步执行任务
go func() {
// 模拟处理逻辑花费时间超长(如需要 MySQL 请求等)
// do.....
time.Sleep(time.Second * 3)
ans := "ayang"
// 问题所在(bug):如果超时了,那么下面会走 timeout case
// 而如果一段时间后上面的逻辑处理完毕,执行下面代码时发现 done 已经没有协程在等待
// 将会永久阻塞,造成 goroutine 永远不会回收,也就是协程泄露
done <- ans
print("子协程正常结束\n")
}()
// select 等待子协程处理完毕或超时直接返回
// 问:既然异步了,为什么这里还要阻塞等待执行完毕?不是还是又回到同步么?
// 确实是同步的,不过同步的只有最多 timeout 的时间,过了这个时间会立刻返回(那个子协程此时就是异步执行了)
// 如果不采取子协程异步发送,那么可能会一直阻塞在该方法很久
// 也就是说实现超时 立刻返回 功能必须是 异步 的
select {
case <-timeout.C:
result = "nil"
case result = <-done:
}
return result
}
|
解决方法
解决方法也是比较简单,直接看里面注释把。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
package main
import "time"
func main() {
result := deal()
print("执行完毕,result 为:", result, "\n")
time.Sleep(time.Second * 5)
print("父协程结束\n")
}
func deal() string {
timeout := time.NewTimer(time.Second * 2)
defer timeout.Stop()
done := make(chan string)
result := ""
go func() {
time.Sleep(time.Second * 3)
ans := "ayang"
// 解决办法:非阻塞写入。
// 因为如果超时了,那么外面已经没有协程阻塞等待接受了,
select {
case done <- ans:
default:
}
print("子协程正常结束\n")
}()
select {
case <-timeout.C:
result = "nil"
case result = <-done:
}
return result
}
|
End