文章目录
- 并发编程
- 1.概述
- 1.1 并行和并发
- 1.2 Go语言并发优势
- 2. goroutine
- 2.1 goroutine的定义
- 2.2 goroutine的创建和运行
- 2.2.1 mian goroutine
- 2.2.2 其他goroutine
- 2.3 runtime包
- 2.3.1 Gosched
- 3.2.3 Goexit
- 2.3.3 GOMAXPROCS
- 3 channel
- 3.1 概述
- 3.2 channel的创建
- 3.3 channel中的数据的发送和接收
- 3.4 无缓冲的channel
- 3.4.1 概述
- 3.4.2 无缓冲通道的两个goroutine交换数据示意
- 3.4.3 无缓冲channel的创建
- 3.4.4 内置函数close在channel中的应用
- 3.4.5 用range迭代channel
- 3.5 有缓冲的channel
- 3.5.1 概述
- 3.5.2 有缓冲通道的两个goroutine交换数据示意图
- 3.5.3 有缓冲channel的创建
- 3.6 单向的channel
- 3.6.1
- 3.6.2 单向channel变量的声明
- 3.6.3 channel的转换
- 4 select
- 4.1 语法结构
- 4.2 select语句执行顺序
- 4.3 select语句中设置超时
并发编程
1.概述
1.1 并行和并发
类别 | 区别 |
并行 | 在多个处理器上同时执行多条指令 |
并发 | 在同一时刻只有一条指令在执行,但多个进程指令被快速轮换执行, 使得宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的。 |
1.2 Go语言并发优势
机制 | 并发优势 |
自动垃圾回收 | 自动管理内存 |
goroutine | 创建一个goroutine代价极小,让并发编程更加轻盈; 通道channel让不同的goroutine直接同步安全地发送消息,让并发编程更加安全。 |
2. goroutine
2.1 goroutine的定义
Rob Pike说:”一个goroutine是一个与其他goroutines并发运行在同一地址空间的Go函数或方法。一个运行的程序由一个或更多个goroutine组成。它与线程、协程、进程等不同。它是一个goroutine。“
所以我们可以认为:goroutine就是一个函数或方法。
2.2 goroutine的创建和运行
- a. goroutine 的创建和运行:
在函数或者方法调用语句之前添加关键字go,就可以创建一个goroutine。开发人员无需了解任何执行细节,调度器会自动将其安排到合适的系统线程上执行。
- b.goroutine的运行:
在一个go程序里,我们将一个过程切分程几块,然后让每个goroutine各自负责一块工作。当一个 程序启动时,其主函数在一个单独的goroutine中运行,这个goroutine叫做main goroutine。新的goroutine会用go语句来 创建。
2.2.1 mian goroutine
在Go语言,主函数运行在main goroutine中,其他goroutine和main goroutine并发运行。如果main goroutine先执行完毕,那么其他的goroutine也会自动退出。
package main
import (
"fmt"
"time"
)
func hello() {
//延迟1秒再执行下面的语句
time.Sleep(1 * time.Second)
fmt.Println("Hello world")
}
/*main goroutine先执行完,其他goroutine自动退出,不执行*/
func main() {
go hello()
fmt.Println("main function")
}
2.2.2 其他goroutine
如果要运行所有其他的goroutine,main goroutine必须处于运行状态,直到其它goroutine运行完毕。
package main
import (
"fmt"
"time"
)
func test1() {
time.Sleep(time.Second) //延迟1秒
fmt.Println("test1")
}
func test2() {
fmt.Println("test2")
}
/* main goroutine延迟2秒,让其它goroutine先执行完,再执行main goroutine*/
func main() {
go test1()
go test2()
time.Sleep(2 * time.Second)
fmt.Println("main function")
}
2.3 runtime包
2.3.1 Gosched
runtime.Gosched()用于让出CPU时间片,让出当前goroutine的执行权,调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行。
3.2.3 Goexit
调用runtime.Goexit(),会立即终止当前goroutine的执行,调度器确保所有已注册defer延迟调用被执行。
2.3.3 GOMAXPROCS
掉头runtime.GOMAXPROCS()用来设置可以并行计算的CPU核数的最大值,并返回之前的值。
- runtime.Gosched()
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
}
}
func main() {
go say("world")
/*暂停当前main goroutine,执行其它goroutine,等其它goroutine执行完,再执行main goroutine*/
runtime.Gosched() //注释掉这一行,会直接输出hello,子 goroutine还没执行main已经结束了
fmt.Println("hello")
}
- runtime.Goexit()
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
runtime.Goexit()
fmt.Println("B")
}()
fmt.Println("A")
}()
for {
}
}
- runtime. GOMAXPROCS()
package main
import (
"fmt"
"runtime"
)
func main() {
//runtime.GOMAXPROCS(1)
runtime.GOMAXPROCS(2)
for {
go fmt.Print(0)
fmt.Print(1)
}
}
分别传入1和2 ,在一个cpu时间片内处处的1和0的连续性有明显区别,前提是你的计算机是双核以上。
3 channel
3.1 概述
当一个资源需要在goroutine直接共享时,channel在goroutine之间架起一个管道,并提供了确保同步交换数据的机制。
3.2 channel的创建
make(chan Type) //等价于make(chan Type, 0)
make(chan Type, capacity)
- a. 定义一个channel,需要定义发送到channel的值的类型;
- b.当capacity=0时,channel是无缓冲阻塞读写的;当capacity>0时,channel是有缓冲、非阻塞的,直到写满capacity个元素才阻塞读写。
3.3 channel中的数据的发送和接收
功能 | 格式 |
将value发送到channel | channel <- value |
接收channel中的值并丢弃 | <- channel |
从channel中接收value并赋值给x | x:= <-channel |
从channel中接收value,并赋值给x;同时 检查通道是否已关闭或者是否为空(ok为false) | x,ok := <-channel |
3.4 无缓冲的channel
3.4.1 概述
- a. 无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道
- b. 同时准备好发送goroutine和接收goroutine,才能完成发送和接收操作
- c. 如果两个goroutine没有同时准备好,通道会导致先执行发送或者接收的goroutine阻塞等待。
- d. 对通道进行发送和接收的交互行为是同步的,其中任意一个操作都无法离开另一个操作单独存在。
3.4.2 无缓冲通道的两个goroutine交换数据示意
- 1 两个goroutine都到达通道两端,但两个都没有开始执行发送数据或者接收数据。
- 2 左侧的goroutine将它的手伸进通道,这模拟了向通道发送数据的行为。此时,这个goroutine会再通道中被锁住,直到交换完成。
- 3 右侧的goroutine都将它的手放入通道,这模拟了从通道里接收数据.这个goroutine也一样会在通道中被锁住,直到交换完成。
- 4.5 第四步和第五步进行数据交换。
- 6 两个goroutine都将他们的手从通道里拿出来,这模拟了被锁住的两个goroutine得到释放。两个goroutine现在都可以去做别的事情了。
3.4.3 无缓冲channel的创建
无缓冲channel没有指定缓冲区容量,那么无缓冲的channel中的数据在发送和接收需要同步,其创建格式如下:
make(chan Type)//等价于make(chan Type, 0)
package main
import (
"fmt"
)
func main() {
c := make(chan int, 0) //无缓冲通道
/*内置函数len返回未被读取的缓冲元素数量,cap返回缓冲区大小*/
fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))
go func() {
defer fmt.Println("子协程结束")
for i := 0; i < 3; i++ {
c <- i
fmt.Printf("子协程正在运行[%d]:len(c)=%d,cap(c)=%d\n", i, len(c), (c))
}
}()
for i := 0; i < 3; i++ {
num := <-c //从c中接收数据并赋值给num
fmt.Println("num = ", num)
}
fmt.Println("main协程结束")
}
3.4.4 内置函数close在channel中的应用
- a. 如果发送这知道没有更多的值发送到channel,那么让接收者也能及时知道没有多余的值可接收将是有用的。因为接收者可以停止不必要的接收等待,这可以通过内置的close函数来实现channel的关闭。
- b. channel不像文件一样需要经常关闭,只有确实没有任何数据发送才关闭channel后,无法向 channel再发送数据。
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
/*把close(c)注释掉,程序会一直阻塞再 if data,ok:= <-c; ok 那一行*/
close(c)
}()
for {
/*ok为true说明channel没有关闭,为false说明通道已经关闭*/
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("Finished")
}
3.4.5 用range迭代channel
类似与range在数组切片和字典中的应用,range也可以迭代操作channel。
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
/*把close(c)注释掉,程序会一直阻塞再 if data,ok:= <-c; ok 那一行*/
close(c)
}()
for data := range c {
fmt.Println(data)
}
fmt.Println("Finished")
}
3.5 有缓冲的channel
3.5.1 概述
- a. 有缓冲的通道(buffered channel)是一种在被接收前能存储一个或多个值的通道。这种类型的通道并不强制要求goroutine之间必须同时完成发送和接收,通道会阻塞发送和接收动作的条件也不同:只有在通道中没有接手值时,接收动作才会阻塞;只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
- b. 有缓冲通道和无缓冲通道之间最大的不同在于:无缓冲通道保证发送和接收的goroutine会在同一时间进行数据交换;有缓冲的通道没有这种保证。
3.5.2 有缓冲通道的两个goroutine交换数据示意图
- 1 右侧的goroutine正在从通道中接收一个值
- 2 右侧的这个goroutine独立完成了接收值的操作,而左侧的goroutine正在发送一个新的值到通道里
- 3 左侧的goroutine向通道发送新值,右侧的goroutine正在从通道j接收另外一个值。这个步骤里的两个操作既不同步,也不会相互阻塞
- 4 所有的发送和接收都完成,而通道里还有几个值,也有一些空间k而已存更多的值。
3.5.3 有缓冲channel的创建
如果给定了一个缓冲区容量,通常就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞的进行。有缓冲区的channel创建格式如下:
make(chan Type, capacity)
package main
import (
"fmt"
)
func main() {
c := make(chan int, 3) //带缓冲的通道
/*内置函数len返回未被读取的缓冲元素数量,cap返回缓冲区大小*/
fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))
go func() {
defer fmt.Println("子协程结束")
for i := 0; i < 3; i++ {
c <- i
fmt.Printf("子协程正在运行[%d]:len(c)=%d,cap(c)=%d\n", i, len(c), (c))
}
}()
for i := 0; i < 3; i++ {
num := <-c //从c中接收数据并赋值给num
fmt.Println("num = ", num)
}
fmt.Println("main协程结束")
}
3.6 单向的channel
3.6.1
channel默认双向,即可以向channel里发送数据也可以从channel里接收数据。但是,要使channel作为参数传递且被单向使用,即只向该channel发送数据,或此channel只接收数据,那么需要对此channel指定方向。
3.6.2 单向channel变量的声明
类型 | 格式 |
单向写入channel | var ch1 chan<- int |
单向读取channel | var ch2 <-chan int |
注:
- a. chan<- 表示把数据写入管道
- b. <-chan 表示将数据从管道中读取
3.6.3 channel的转换
普通的channel可以隐式转换为单向的channel,只接收或只发送数据。但是单向的channel不能转换为普通的channel。
c := make(chan int, 3)
var send chan<- int = c //只能向管道发送数据,即写入
var recv <-chan int = c //只能从管道接收数据,即读取
package main
import "fmt"
//chan<-:只写
func counter(write chan<- int) {
defer close(write)
for i := 0; i < 5; i++ {
write <- i //如果对方不读会阻塞
}
}
//<-chan:只读
func printer(read <-chan int) {
for num := range read {
fmt.Println(num)
}
}
func main() {
c := make(chan int) //chan:默认双向——读写
//将普通channel隐式转换为单向channel——数据写入
go counter(c)
/*将普通channel隐式转换为单向channel——数据从channel里读取*/
printer(c)
fmt.Println("done")
}
4 select
4.1 语法结构
- a. 关键字select可以监听channel上的数据流动。
- b. 用法与switch语句相似,但select语句中的每个case必须是一个IO(输入/输出)操作。
select {
case <-chan1:
/*如果chan1成功读取到数据,则进行该case处理语句*/
case chan2 <- 1:
/*如果chan2成功读取到数据,则进行该case处理语句*/
default:
/*如果上面都没有成功,则进入default处理流程*/
}
4.2 select语句执行顺序
在select语句中,Go语言按顺序从头职位评估每一个发送和接收的语句:
- a. 如果其中任意一条语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。
- b. 如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能:
第一种,如果有default语句,那么就会执行default语句,同时程序执行会从select语句后的语句中恢复
第二种,如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。
package main
import (
"fmt"
"time"
)
func server1(ch chan string) {
time.Sleep(6 * time.Second)
ch <- "from server1"
}
func server2(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}
package main
import (
"fmt"
"time"
)
func process(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "process successful"
}
func main() {
ch := make(chan string)
go process(ch)
for {
time.Sleep(time.Second)
select {
case v := <-ch:
fmt.Println("received value: ", v)
return
default:
fmt.Println("no value received")
}
}
}
4.3 select语句中设置超时
当goroutine阻塞时,为避免整个程序进入阻塞,可以在select语句设置超时,具体通过如下方式实现:
case <-time.After(5 * time.Second)://这个case延时5秒执行
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <-c:
fmt.Println(v)
case <-time.After(5 * time.Second):
fmt.Println("timeout")
o <- true
}
}
}()
c <- 666
<-o
}