写点什么

Go 语言快速入门指南:Go 并发初识

作者:宇宙之一粟
  • 2022 年 1 月 17 日
  • 本文字数:3215 字

    阅读完需:约 11 分钟

大型程序通常由许多较小的子程序组成。 例如,Web 服务器处理来自 Web 浏览器的请求并提供 HTML 网页作为响应。 每个请求都像一个小程序一样被处理。对于像这样的程序,最理想的是能够在同一时间运行它们的小型组件(在 网络服务器的情况下,处理多个请求)。同时在一个以上的任务上取得进展被称为并发性。


线程

线程是操作系统为您提供的一项功能,可让您并行运行程序的各个部分。 假设您的程序由两个主要部分组成,第 1 部分和第 2 部分,并且您编写的代码使得第 1 部分在 线程一 上运行,而第 2 部分在 线程二 上运行。 在这种情况下,程序的两个部分将同时并行运行; 下图说明了它的外观:



现代软件中真正独立的线程数量与程序需要执行的并发软件数量之间存在差距。 在现代软件中,您可能需要数千个程序同时独立运行,即使您的操作系统可能只提供四个线程!

什么叫并发

并发指在同一时间内可以执行多个任务。并发编程含义比较广泛,包含多线程编程、多进程编程及分布式程序等。


在 Go 中,并发意味着您的程序能够将自身切割成更小的部分,然后能够在不同时间运行不同的独立部分,目标是根据可用资源的数量尽快执行所有任务。


Go 中使用 goroutineschannel 来支持并发。

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.Sleeprand.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.gofrom c2from c1from c1from c2from c1from c2from c1from c1from c2from c1from c2from c1from c1from 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 的缓冲通道。通常通道是同步的; 通道的双方将等待,直到另一方准备就绪。


缓冲通道是异步的; 除非通道已满,否则发送或接收消息不会等待。


总结

本文只是简单的介绍了并发的相关知识,然后介绍了 Go 语言原生支持的 goroutineschannel 的概念与应用,下一篇文章将从更深入的运用层面学习 Go 并发。

发布于: 刚刚阅读数: 4
用户头像

宇宙古今无有穷期,一生不过须臾,当思奋争 2020.05.07 加入

🏆InfoQ写作平台-第二季签约作者 🏆 混迹于江湖,江湖却没有我的影子 热爱技术,专注于后端全栈,轻易不换岗 拒绝内卷,工作于软件工程师,弹性不加班 热衷分享,执着于阅读写作,佛系不水文

评论

发布
暂无评论
Go 语言快速入门指南:Go 并发初识