大型程序通常由许多较小的子程序组成。 例如,Web 服务器处理来自 Web 浏览器的请求并提供 HTML 网页作为响应。 每个请求都像一个小程序一样被处理。对于像这样的程序,最理想的是能够 在同一时间运行它们的小型组件(在 网络服务器的情况下,处理多个请求)。同时在一个以上的任务上取得进展 被称为并发性。
线程
线程是操作系统为您提供的一项功能,可让您并行运行程序的各个部分。 假设您的程序由两个主要部分组成,第 1 部分和第 2 部分,并且您编写的代码使得第 1 部分在 线程一 上运行,而第 2 部分在 线程二 上运行。 在这种情况下,程序的两个部分将同时并行运行; 下图说明了它的外观:
现代软件中真正独立的线程数量与程序需要执行的并发软件数量之间存在差距。 在现代软件中,您可能需要数千个程序同时独立运行,即使您的操作系统可能只提供四个线程!
什么叫并发
并发指在同一时间内可以执行多个任务。并发编程含义比较广泛,包含多线程编程、多进程编程及分布式程序等。
在 Go 中,并发意味着您的程序能够将自身切割成更小的部分,然后能够在不同时间运行不同的独立部分,目标是根据可用资源的数量尽快执行所有任务。
Go 中使用 goroutines 和 channel 来支持并发。
Goroutines
一个 goroutine 是一个能够与其他函数同时运行的函数。与其他函数同时运行。要创建一个 goroutine 我们使用关键字 go
,后面跟着一个函数调用:
package main import "fmt" func f(n int) { for i := 0; i < 10; i++ { fmt.Println(n, ":", i) } } func main() { go f(0) var input string fmt.Scanln(&input) }
该程序由两个 goroutine 组成。 第一个 goroutine 是隐式的,是主函数本身。 当我们调用 go f(0)
时会创建第二个 goroutine。 通常,当我们调用一个函数时,我们的程序会执行函数中的所有语句,然后返回到调用后的下一行。 使用 goroutine,我们立即返回到下一行,而不是等待函数完成。 这就是调用 Scanln 函数的原因; 没有它,程序将在有机会打印所有数字之前退出。
Goroutines 是轻量级的,我们可以轻松地创建数以千计的 Goroutines。 我们可以通过这样做来修改我们的程序,以运行 10 个 goroutines:
func main() { for i := 0; i < 10; i++ { go f(i) } var input string fmt.Scanln(&input) }
你可能已经注意到,当你运行这个程序时,它似乎是按顺序而不是同时运行 goroutine。 让我们使用 time.Sleep
和 rand.Intn
为函数添加一些延迟:
package main import ( "fmt" "math/rand" "time" ) func f(n int) { for i := 0; i < 10; i++ { fmt.Println(n, ":", i) amt := time.Duration(rand.Intn(250)) time.Sleep(time.Millisecond * amt) } } func main() { for i := 0; i < 10; i++ { go f(i) } var input string fmt.Scanln(&input) }
f
打印出从0到10的数字,在每个数字之后等待 0到250毫秒之间。这些程序现在应该同时运行。
Channels
通道为两个 goroutine 提供了一种通信方式,并使它们的执行同步。下面是一个使用通道的示例程序:
该程序将永远打印 “ping”(按回车键停止)。 通道类型用关键字 chan
表示,后跟通道上传递的事物的类型(在这种情况下,我们传递的是字符串)。 <-
(左箭头)运算符用于在通道中发送和接收消息。
package main import ( "fmt" "time" ) func pinger(c chan string) { for i := 0; ; i++ { c <- "ping" } } func printer(c chan string) { for { msg := <-c fmt.Println(msg) time.Sleep(time.Second * 1) } } func main() { var c chan string = make(chan string) go pinger(c) go printer(c) var input string fmt.Scanln(&input) }
c <- "ping"
表示着发送 “ping”。
msg := <- c
表示接收一个消息并把这个消息保存到 msg
中。
fmt
一行中也可以被这样写: fmt.Println(<-c)
,这样的话 msg := <- c
就可以删掉了。
使用这样的通道可以同步两个 goroutine。 当 pinger 尝试在通道上发送消息时,它将等待 printer 准备好接收消息。 (这被称为阻塞)让我们向程序添加另一个发送者,看看会发生什么。 添加这个功能:
package main import ( "fmt" "time" ) func pinger(c chan string) { for i := 0; ; i++ { c <- "ping" } } func ponger(c chan string) { for i := 0; ; i++ { c <- "pong" } } func printer(c chan string) { for { msg := <-c fmt.Println(msg) time.Sleep(time.Second * 1) } } func main() { var c chan string = make(chan string) go pinger(c) go ponger(c) go printer(c) var input string fmt.Scanln(&input) }
该程序现在将轮流打印“ping”和“pong”。
通道方向
我们可以在通道类型上指定一个方向,从而将其限制为发送或接收。 例如 pinger 的函数签名可以改成这样:
func pinger(c chan<- string)
此时,c
只能发送,如果尝试从 c
接收的话会导致编译出错。同样的我们可以更改 printer
:
func printer(c <-chan string)
没有这些限制的通道称为双向通道。 可以将双向通道传递给采用仅发送或仅接收通道的函数,但反之则不然。
Select
Go 有一个名为 select 的特殊语句,它的工作方式类似于 switch ,但只适用于通道:
package main import ( "fmt" "time" ) func main() { c1 := make(chan string) c2 := make(chan string) go func() { for { c1 <- "from c1 " time.Sleep(2 * time.Second) } }() go func() { for { c2 <- "from c2 " time.Sleep(3 * time.Second) } }() go func() { for { select { case msg1 := <-c1: fmt.Println(msg1) case msg2 := <-c2: fmt.Println(msg2) } } }() var input string fmt.Scanln(&input) }
该程序每 2 秒打印一次“from c1”,每 3 秒打印一次“from c2”。 select
选择第一个准备好的通道并从它接收(或发送到它)。 如果多个通道准备就绪,则它会随机选择要接收的通道。 如果没有通道准备好,则语句阻塞,直到一个通道可用。
$ go run main.go from c2 from c1 from c1 from c2 from c1 from c2 from c1 from c1 from c2 from c1 from c2 from c1 from c1 from c2
select 语句通常用于实现超时:
select { case msg1 := <- c1: fmt.Println("Message 1", msg1) case msg2 := <- c2: fmt.Println("Message 2", msg2) case <- time.After(time.Second): fmt.Println("timeout") }
time.After 创建一个通道,在给定的持续时间之后将在其上发送当前时间。 (我们对时间不感兴趣,所以我们没有将它存储在变量中)我们还可以指定一个默认情况:
select { case msg1 := <- c1: fmt.Println("Message 1", msg1) case msg2 := <- c2: fmt.Println("Message 2", msg2) case <- time.After(time.Second): fmt.Println("timeout") default: fmt.Println("nothing ready") }
默认情况下,如果没有任何一个通道准备好。
缓冲通道
也可以在创建通道时将第二个参数传递给 make 函数:
c := make(chan int, 1)
这将创建一个容量为 1 的缓冲通道。通常通道是同步的; 通道的双方将等待,直到另一方准备就绪。
缓冲通道是异步的; 除非通道已满,否则发送或接收消息不会等待。