通知可以被看作特殊的异步请求,这类请求的结果不重要,但是否完成很重要。一般来说,可以使用一个空的struct{}
作为channel
的元素类型。因为struct{}
的大小是0,所以不怎么占用内存。
如果channel
里并没有接受到任何值,接收操作会阻塞,直到另一个goroutine
发送了一个值给这个channel
。所以通知方可以通过发送一个值给channel
的方法,来通知另一个goroutine
。而被通知方只需要等待接收同一个channel
即可。
在以下例子中,名字叫done
的channel
被用来通知。
package main
import (
"crypto/rand"
"fmt"
"os"
"sort"
)
func main() {
values := make([]byte, 32 * 1024 * 1024)
if _, err := rand.Read(values); err != nil {
fmt.Println(err)
os.Exit(1)
}
done := make(chan struct{}) // 阻塞与否都行
// 用来排序的goroutine
go func() {
sort.Slice(values, func(i, j int) bool {
return values[i] < values[j]
})
// 通知排序完成了。
done <- struct{}{}
}()
// 干点别的……
<- done // 等待接收通知
fmt.Println(values[0], values[len(values)-1])
}
如果一个channel
缓冲区满了(一个阻塞的channel
缓冲区永远是满的),发送请求操作会阻塞,直到另一个goroutine
接收这个channel
的请求。所以,通知方可以接收channel
的数据,用以通知另一goroutine
。而被通知方会往channel
发送数据,直到发送成功。一般来说,这种channel
是阻塞的。
比起上个例子,这种方法不太常用:
package main
import (
"fmt"
"time"
)
func main() {
done := make(chan struct{})
// 其实这里的缓冲区大小也可以设置为1。如果这样设置的话,需要
// 先发送一个值,再创建下面的goroutine。
go func() {
fmt.Print("Hello")
// 模拟延迟。
time.Sleep(time.Second * 2)
// 从done这个channel接收一个值,这样主goroutine就不
// 再被发送操作阻塞。
<- done
}()
// 这里会阻塞,等待通知。
done <- struct{}{}
fmt.Println(" world!")
}
事实上,以上两种1对1的通知方法,没有本质区别。都是快的操作等待被慢的操作通知。
稍微扩展一下,就可以做N对1、1对N的通知:
package main
import "log"
import "time"
type T = struct{}
func worker(id int, ready <-chan T, done chan<- T) {
<-ready // 阻塞在这里,等待被通知
log.Print("Worker#", id, " starts.")
// 模拟延迟
time.Sleep(time.Second * time.Duration(id+1))
log.Print("Worker#", id, " job done.")
// 发送通知给主goroutine(N对1的通知)。
done <- T{}
}
func main() {
log.SetFlags(0)
ready, done := make(chan T), make(chan T)
go worker(0, ready, done)
go worker(1, ready, done)
go worker(2, ready, done)
// 模拟初始化的时间。
time.Sleep(time.Second * 3 / 2)
// 发送1对N的通知。
ready <- T{}; ready <- T{}; ready <- T{}
// 等待接受N对1的通知。
<-done; <-done; <-done
}
事实上,以上用法并不常见。实际应用中,我们常用sync.WaitGroup
来做N对1的通知。而1对N的通知,可以直接通过关闭channel
来实现。下面会仔细介绍。
上面例子的1对N通知的用法现实中很少用到,因为有更好的方法。一个已经关闭的channel
可以用来接受无数个值,我们可以根据这个特性来实现广播通知。
只需要把上个例子中的ready <- struct{}{}
换成close(ready)
即可。
...
close(ready) // 广播通知
...
当然,就算1对1通知,也可以用关闭channel
来实现。事实上,这也是非常常见的用法。
一个已经关闭的channel
可以用来接受无数个值,这个特性会被用来做很多事情,我们以后会介绍。事实上,在标准库中,这个特性也经常被用到。比如context
库用它来确认操作是否被取消。
很容易通过channel
进行定时通知:
package main
import (
"fmt"
"time"
)
func AfterDuration(d time.Duration) <- chan struct{} {
c := make(chan struct{}, 1)
go func() {
time.Sleep(d)
c <- struct{}{}
}()
return c
}
func main() {
fmt.Println("Hi!")
<- AfterDuration(time.Second)
fmt.Println("Hello!")
<- AfterDuration(time.Second)
fmt.Println("Bye!")
}
事实上,time
库中的After
函数提供了类似的功能。(当然,它的实现效率高了很多。)我们应该尽量使用标准库中的函数,这样代码看着比较整洁。
注意,<-time.After(aDuration)
会导致当前goroutine
进入阻塞状态,而time.Sleep(aDuration)
不会。
<-time.After(aDuration)
经常会被用来处理超时,我们以后会详细介绍。