GO语言并发编程

并行和并发

并行:指在同一时刻,有多条指令在多个处理器上同时执行

并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,宏观同时执行,微观快速交替执行

GO语言并发的优势

从语言层面就支持了并发。go提供了自动垃圾回收机制,不用关心内存管理问题

goroutine

goroutine是什么

它是GO并发设计的核心,是协程,但是比线程更小,可以同时运行成千上万个并发任务

协程和线程的区别

线程:cpu切换多个进程的时候,会花费不少的时间,因为切换进程需要切换到内核态,而每次调度需要内核态都需要读取用户态的数据,进程一旦多起来,cpu调度会消耗一大堆资源,因此引入了线程的概念,线程本身几乎不占有资源,他们共享进程里的资源,内核调度起来不会那么像进程切换那么耗费资源。
总结:线程是用来进行内核调度进行共享资源,系统执行。
协程:协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作执行者则是用户自身程序,goroutine也是协程。
总结:拥有属于自己的寄存器,在切换回来时恢复保存在上下文的栈,用户自身程序执行。
区别:线程和协同程序的主要不同在于:在多处理器情况下,从概念上来讲多线程程序同时运行多个线程;
而协同程序是通过协作来完成,在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只在必要时才会被挂起

创建goroutine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func NewTest(){
for {
fmt.Println("这是一个新任务")
time.Sleep(time.Second) //延时一秒
}
}

func main(){
go NewTest() //新建一个协程
for {
fmt.Println("这是一个主协程")
time.Sleep(time.Second) //延时一秒
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//主协程退出了,子协程也要跟着退出

func main() {
i:=0
go func() {
for{
i++
fmt.Println("子协程 i= ",i)
time.Sleep(time.Second)

if i == 10{
break
}
}
}()

j := 0
for{
j++
fmt.Println("main j= ",j)
time.Sleep(time.Second)

if j == 2{
break
}
}
}

runtime包

Gosched

用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
go func() {
for i:=0;i<5;i++{
fmt.Println("子协程")
}
}()

for i:=0;i<2;i++{
//让出时间片,让子协程先执行,等子协程执行完,再执行主协程
runtime.Gosched()
fmt.Println("main")
}
}

Goexit

立即终止当前协程执行,调度器确保所有已注册的defer延迟调度被执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func test()  {
defer fmt.Println("world")
runtime.Goexit()//终止所在的协程
fmt.Println("hello world")
}

func main() {
go func() {
fmt.Println("子协程")
test()
fmt.Println("hello")
}()

for {//目的让主函数不结束
}
}

GOMAXPROCS

用来设置可以并发计算的CPU核数的最大值,并返回之前的值

1
2
3
4
5
6
7
8
9
func main() {
n:=runtime.GOMAXPROCS(1/*2,3,4*/)//指定单核运算,返回之前CPU核数
fmt.Println(n)

for{
go fmt.Print(1)
fmt.Print(0)
}
}

channel

goroutine运行在相同的地址空间,因此访问共享内存必须做好同步,goroutine奉行通过通信来共享内存,而不是通过共享内存来通信。

引用channel是CSP模式的具体体现,用于多个goroutine通讯,其内部实现了同步,确保并发安全。

默认情况下,channel接收和发送数据都是阻塞的,除非是另一端已经准备好,这样就使得goroutine同步变得更加简单,而不是显式的lock

通过channel实现同步和数据交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//channel通过操作符<-来接收和发送数据
//全局变量,创建一个channel
var ch = make(chan int)

//打印机
func Printer(str string) {
for _,data:=range str{
fmt.Printf("%c",data)
time.Sleep(time.Second)
}
fmt.Println()
}

//person1执行完以后person2才执行
func person1() {
Printer("hello")
ch <- 666 //给管道写数据,发送
}

func person2() {
<-ch//从管道取数据,接收,如果通道没有数据,就阻塞
Printer("world")
}

func main() {
go person1()
go person2()

for{
}
}

无缓冲的channel

无缓冲的通道是指在接收前没有能力保存任何值的通道

这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作,如果两个goroutine没有同时准备好,通道会导致先执行发送或接收操作的goroutine阻塞等待

无缓冲的channel创建格式

1
make (chan Type) //等价于make (chan Type , 0)

如果没有指定缓冲区容量,那么该通道就是同步的,因此会阻塞发送者准备好发送和接收者准备好接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
//创建一个无缓冲的channel
ch := make(chan int)

fmt.Printf("len(ch) = %d , cap(ch) = %d\n",len(ch),cap(ch))
//新建协程
go func() {
for i:=0 ; i<3 ; i++{
fmt.Println("子协程 i = ",i)
ch <- i//往channel写内容
}
}()
time.Sleep(2*time.Second)

for i:=0 ; i<3 ;i++{
num := <- ch //读管道中内容
fmt.Println("num = ",num)
}
}

有缓冲的channel

有缓冲的通道是一种在被接收前能存储一个或多个值的通道

它不需要发送goroutine和接收goroutine同时准备好,就完成发送和接收操作。只有在通道中没有要接收的值时接收才会阻塞,在没有缓冲区容纳被发送的值时发送才会阻塞。

有缓冲的channel创建格式

1
make(chan Type ,capacity) //指定容量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
//创建一个有缓冲的channel
ch := make(chan int , 3)

fmt.Printf("len(ch) = %d , cap(ch) = %d\n",len(ch),cap(ch))
//新建协程
go func() {
for i:=0 ; i<3 ; i++{
fmt.Println("子协程 i = ",i)
ch <- i//往channel写内容
fmt.Printf("len(ch) = %d , cap(ch) = %d\n",len(ch),cap(ch))
}
}()
time.Sleep(2*time.Second)

for i:=0 ; i<3 ;i++{
num := <- ch //读管道中内容
fmt.Println("num = ",num)
}
}

注:打印的结果并不能表现为过程,因为写入内容到channel的时候可能读取内容的打印信息还没有执行完

关闭channel

1
2
var ch = make(chan Type)
close(ch)

channel关闭后,不可写入,但可以继续读

单向channel

默认情况下,通道是双向的,既可以发送数据也可以接收数据。若要求只让它接收数据或发送数据,就需要指定通道方向

1
2
3
var ch chan int  //双向
var ch chan <- float64 //单向,只用于写float64数据
var ch <-chan int //单向,只用于读int数据

定时器

Timer

1
2
3
4
5
6
7
8
func main() {
//创建一个定时器,设置时间为2S,2S后会往通道写内容
timer := time.NewTimer(2*time.Second)
fmt.Println("当前时间",time.Now())
t := <- timer.C
fmt.Println("t = ",t)
}
//timer只会响应一次
1
2
3
4
5
func main() {
//创建一个定时器,设置时间为2S,2S后会往通道写内容
<-time.After(2*time.Second)
fmt.Println("时间到")
}
1
2
3
4
func main() {
time.Sleep(2*time.Second)
fmt.Println("时间到")
}
1
2
//定时器停止
timer.Stop()
1
2
//定时器重置
timer.Reset(1*time.Second)

Ticker

Ticker是一个定时触发的计时器,它会以一个间隔往channel发送一个事件(当前时间),而channel的接收者可以以固定的时间间隔从channel中读取事件

1
2
3
4
5
6
7
func main() {
ticker := time.NewTicker(1 * time.Second)
for{
t := <- ticker.C
fmt.Println("time = " , t)
}
}

select

通过select可以监听channel的数据流动

语法与switch相似,不过select的每个case语句必须是一个IO操作,如果没有default语句,select将被阻塞,直到至少有一个通信可以进行下去

select实现fibonaacci数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"fmt"
)

//1 1 2 3 5 8 .....

func fibonacci(ch chan <- int ,quit <- chan bool) { //一个只写,一个只读
x,y := 1,1
for{
//监听channel数据的流动
select {
case ch<-x:
x,y = y ,x+y
case flag := <-quit:
fmt.Println("flag = ",flag)
return
}
}
}

func main() {
ch := make(chan int) //数字通信
quit := make(chan bool) //程序是否结束

//消费者,从channel读取内容
go func() {
for i:=0 ; i< 8 ;i++ {
num := <- ch
fmt.Println("num = ",num)
}
quit <- true
}()
//生产者,往channel中写入内容
fibonacci(ch,quit)
}

超时

有时候会出现goroutine阻塞的情况,我们可以利用select来设置超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
c := make(chan int) //数字通信
o := make(chan bool) //程序是否结束
go func() {
for {
select {
case i:= <- c:
fmt.Println(i)
case <-time.After(5*time.Second):
fmt.Println("超时了")
o <- true
return
}
}
}()
c <- 1
<- o //不取主线程就跑完了
}