将 channel 用作通信机制
golang学习笔记——将 channel 用作通信机制
golang学习笔记——并发计算斐波纳契数
Go 中的 channel 是 goroutine 之间的通信机制。 请记住 Go 的并发方法是:不是通过共享内存通信;而是通过通信共享内存。当你需要将值从一个 goroutine 发送到另一个时,可以使用通道。 让我们看看它们的工作原理,以及如何开始使用它们来编写并发 Go 程序。
Channel 语法
ch <- x // sends (or writes ) x through channel ch
x = <-ch // x receives (or reads) data sent to the channel ch
<-ch // receives data, but the result is discarded
关闭 channel
close(ch)
无缓冲 channel
使用 make()
函数创建 channel 时,会创建一个无缓冲 channel,这是默认行为。 无缓冲 channel 会阻止发送操作,直到有人准备好接收数据。
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
start := time.Now()
apis := []string{
"https://mp.csdn.net/",
"https://dev.azure.com",
"https://api.somewhereintheinternet.com/",
"https://gitcode.net/",
}
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
for i := 0; i < len(apis); i++ {
fmt.Print(<-ch)
}
elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}
func checkAPI(api string, ch chan string) {
_, err := http.Get(api)
if err != nil {
ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
return
}
ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}
缓冲 channels
下面是一个理解有缓冲 channel 的简单示例
package main
import (
"fmt"
)
func send(ch chan string, message string) {
ch <- message
}
func main() {
size := 4
ch := make(chan string, size)
send(ch, "one")
send(ch, "two")
send(ch, "three")
send(ch, "four")
fmt.Println("All data sent to the channel ...")
for i := 0; i < size; i++ {
fmt.Println(<-ch)
}
fmt.Println("Done!")
}
输出
All data sent to the channel ...
one
two
three
four
Done!
试着将size改为2
重新运行程序时,将看到以下错误:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.send(...)
D:/golang2023/main.go:8
main.main()
D:/golang2023/main.go:16 +0x97
exit status 2
channel 与 goroutine
channel 与 goroutine 有着紧密的联系。 如果没有另一个 goroutine 从 channel 接收数据,则整个程序可能会永久处于被阻止状态。 正如你所见,这种情况确实会发生。
缓冲 channels 示例
使用之前用于检查 API 的示例,并创建大小为 10 的缓冲通道
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
start := time.Now()
apis := []string{
"https://management.azure.com",
"https://dev.azure.com",
"https://mp.csdn.net/",
"https://outlook.office.com/",
"https://api.somewhereintheinternet.com/",
"https://gitcode.net/",
}
ch := make(chan string, 10)
for _, api := range apis {
go checkAPI(api, ch)
}
for i := 0; i < len(apis); i++ {
fmt.Print(<-ch)
}
elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}
func checkAPI(api string, ch chan string) {
_, err := http.Get(api)
if err != nil {
ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
return
}
ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}
多路复用
最后,让我们讨论如何使用 select
关键字与多个通道同时交互。 有时,在使用多个 channel 时,需要等待事件发生。 例如,当程序正在处理的数据中出现异常时,可以包含一些逻辑来取消操作。
select
语句的工作方式类似于 switch
语句,但它适用于 channel。 它会阻止程序的执行,直到它收到要处理的事件。 如果它收到多个事件,则会随机选择一个。
select
语句的一个重要方面是,它在处理事件后完成执行。 如果要等待更多事件发生,则可能需要使用循环。
让我们使用以下程序来看看 select
的运行情况:
package main
import (
"fmt"
"time"
)
func process(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "Done processing!"
}
func replicate(ch chan string) {
time.Sleep(1 * time.Second)
ch <- "Done replicating!"
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go process(ch1)
go replicate(ch2)
for i := 0; i < 2; i++ {
select {
case process := <-ch1 :
fmt.Println(process)
case replicate := <-ch2 :
fmt.Println(replicate)
}
}
}
输出
Done replicating!
Done processing!
请注意,replicate
函数首先完成,这就是首先在终端中看到其输出的原因。 main 函数存在一个循环,因为 select
语句在收到事件后立即结束,但我们仍在等待 process
函数完成。