一、并发和并行
Go语言为并发编程而内置的上层API基于CSP模型。
communicating sequential processes:顺序通信模型
Go语言通过安全的通道发送和接受数据以实现同步。
一般情况下,一个普通的计算机跑十几二十个线程就有点负载过大了,但是同样这台机器却可以轻松地让成百上千甚至上万个goroutine进行资源竞争。
二、goroutine
goroutine是Go并发设计的核心。
Goroutine说到底其实就是协程,但是他比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。
执行goroutine只需要极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。
Goroutine比thread更易用、更高效、更轻便。
2.1、goroutine退出方式
主协程退出了,其他协程也要跟着退出。
package main
import (
"fmt"
"time"
)
//主协程退出了,子协程也要跟着退出
func main() {
go func() {
i := 0
for {
i++
fmt.Println("子协程 i = ", i)
time.Sleep(time.Second)
}
}()
i := 0
for {
i++
fmt.Println("当主协程 i = ", i)
time.Sleep(time.Second)
if i == 2 { //当主协程中i=2时,主协程退出
break
}
}
}
输出
当主协程 i = 1
子协程 i = 1
子协程 i = 2
当主协程 i = 2
2.2、Runtime.Gosched
用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其他等待的任务执行,并在下次某个时候从该位置恢复执行。
就像接力赛,A跑了一会碰到代码runtime.Gosched()就把接力棒交给了B了,A就歇着了,B继续跑。
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
for i := 0; i < 10; i++ {
fmt.Println("子协程-->:", i)
}
}()
for i := 0; i < 2; i++ {
runtime.Gosched()
fmt.Println("-------让出时间片,先让别子协程执行,执行完后,再执行主协程-------")
fmt.Println("主协程-->:", i)
}
}
输出
子协程-->: 0
子协程-->: 1
子协程-->: 2
子协程-->: 3
子协程-->: 4
子协程-->: 5
子协程-->: 6
子协程-->: 7
子协程-->: 8
子协程-->: 9
-------让出时间片,先让别子协程执行,执行完后,再执行主协程-------
主协程-->: 0
-------让出时间片,先让别子协程执行,执行完后,再执行主协程-------
主协程-->: 1
2.3、runtime.Goexit
立即终止当前goroutine
调度器确保所有已注册defer延迟调用被执行。
package main
import (
"fmt"
"runtime"
)
func test() {
defer fmt.Println("------调度器确保所有已注册defer延迟调用被执行-----")
// return //此函数
fmt.Println("------Goexit执行--->:前-----")
runtime.Goexit() //终止所在的协程
fmt.Println("------Goexit执行--->:后-----")
}
func main() {
go func() {
fmt.Println("------test执行--->:前-----")
test()
fmt.Println("------test执行--->:后-----")
}()
//写一个死循环,让主协程不结束
for {
}
}
输出
------test执行--->:前-----
------Goexit执行--->:前-----
------调度器确保所有已注册defer延迟调用被执行-----
2.4、runtime.GOMAXPROCS()
用来设置用来并行计算的CPU核数最大值,并返回之前的值。
package main
import (
"fmt"
"runtime"
)
func cpu1() {
//设置当前的cpu核数1,并返回之前pcu核数
oldCpuNum := runtime.GOMAXPROCS(1)
fmt.Println("cpu1-->之前pcu核数:", oldCpuNum)
for {
go fmt.Print(1)
fmt.Print(0)
}
}
func cpu2() {
//设置当前的cpu核数2,并返回之前pcu核数
oldCpuNum := runtime.GOMAXPROCS(2)
fmt.Println("cpu2-->之前pcu核数:", oldCpuNum)
for {
go fmt.Print(1)
fmt.Print(0)
}
}
func main() {
cpu1()
//cpu2()
}
cpu1结果
当runtime.GOMAXPROCS(1)时,最多同时只能有一个goroutine被执行。所以会打印很多1。过了一段时间后,GO语言调度器将其置为休眠,并唤醒另一个goroutine,这时候开始打印很多个0了
在打印的时候,goroutine是被调度到操作系统线程上的。
cpu2结果
当runtime.GOMAXPROCS(2)时,我们使用了2个CPU,所以两个goroutine可以一起被执行,以同样的频率交替打印0和1。
三、channel
Goroutine运行在相同的地址空间,因此访问内存必须做好同步。
Goroutine奉行通过通信来共享内存,而不是共享内存来通信。
引用类型channel是CSP模式的具体实现,用于多个goroutine通讯。其内部实现了同步,确保并发安全。
当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。
和其他的引用类型一样,channel的零值也是nil。
3.1、产生资源抢占案例
下面的代码会产生资源抢占
package main
import (
"fmt"
"time"
)
//打印机
func Printer(str string) {
for _, char := range str {
fmt.Printf("%c", char)
fmt.Println("--")
//每秒打印一个字符
time.Sleep(time.Second)
}
}
//person1执行完成之后,person2执行
func person1() {
Printer("123456")
}
func person2() {
Printer("abcdef")
}
func main() {
//创建两个协程,代表两个人,两个人同时使用打印机
go person1()
go person2()
//不让主协程结束,死循环
for {
}
}
输出
a--
1--
2--
b--
c--
3--
4--
d--
e--
5--
6f--
--
3.2、通过管道通信解决资源抢占问题
下面代码,函数都是交替执行
package main
import (
"fmt"
"time"
)
//全局变量,创建一个channel
var ch = make(chan int)
//打印机
func Printer(str string) {
for _, char := range str {
fmt.Printf("%c", char)
fmt.Println("--")
//每秒打印一个字符
time.Sleep(time.Second)
}
}
//person1执行完成之后,person2执行
func person1() {
Printer("123456")
//写数据到管道
//当管道有数据,写数据就会堵塞
ch <- 666
}
func person2() {
//从管道读数据
//如果通道没有数据,就会阻塞
<-ch
Printer("abcdef")
}
func main() {
//创建两个协程,代表两个人,两个人同时使用打印机
go person1()
go person2()
//不让主协程结束,死循环
for {
}
}
输出
1--
2--
3--
4--
5--
6--
a--
b--
c--
d--
e--
f--
3.3、通过channel实现同步和数据交互
package main
import (
"fmt"
"time"
)
func main() {
//创建channel
ch := make(chan string)
defer fmt.Println("主协程结束")
go func() {
defer fmt.Println("子协程执行-->完毕")
fmt.Println("子协程执行-->开始")
for i := 1; i <= 2; i++ {
time.Sleep(time.Second)
fmt.Println("子协程睡眠:", i, "秒")
}
ch <- "子协程end"
}()
str := <-ch //没有数据前,阻塞
fmt.Println("管道内容:", str)
}
输出
子协程执行-->开始
子协程睡眠: 1 秒
子协程睡眠: 2 秒
子协程执行-->完毕
管道内容: 子协程end
主协程结束
四、定时器
Timer是一个定时器,代表未来的一个单一事件,你可以告诉timer你要等待多长时间,它提供一个channel,在将来的那个时间给那个channel写入一个时间值。
4.1、通过timer实现延时:
package main
import (
"fmt"
"time"
)
func main() {
//创建一个定时器,设置时间为2s,2s后,往time通道写内容(当前时间)
//timer只会执行一次
timer := time.NewTimer(2 * time.Second)
fmt.Println("当前时间:", time.Now())
//2s后,往timer.C写数据,有数据后,就可以读取
t := <-timer.C
fmt.Println("当前时间: ", t)
}
输出
当前时间: 2021-06-20 20:11:25.6545682 +0800 CST m=+0.004318801
当前时间: 2021-06-20 20:11:27.6555197 +0800 CST m=+2.005270301
4.2、停止定时器:
package main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(2 * time.Second)
fmt.Println("当前时间:", time.Now())
go func() {
<-timer.C
fmt.Println("子协程可以打印了,因为定时器的时间到")
}()
timer.Stop() //停止定时器
for {
}
}
4.3、重置定时器:
package main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(60 * time.Second)
fmt.Println("当前时间:", time.Now())
timer.Reset(1 * time.Second) //重置计时器
t := <-timer.C
fmt.Println("当前时间:", t)
}
4.4、定时触发计时器
Ticker是一个定时触发的计时器,他会以一个间隔(interval)往channel发送一个事件(当前时间),而channel的接收者可以以固定的时间间隔从chnanel中读取事件。
package main
import (
"fmt"
"time"
)
func main() {
//创建定时器,每隔1秒后,定时器就会给channel发送一个时间(当前时间)
ticker := time.NewTicker(1 * time.Second)
go func() {
for t := range ticker.C {
fmt.Println("Tick at", t)
}
}()
//一旦一个打点停止了,将不能再从它的通道中接收到值。我们将在运行后 1600ms停止这个打点器。
time.Sleep(time.Second * 10)
ticker.Stop()
fmt.Println("Ticker stopped")
}
输出
Tick at 2021-06-20 20:44:10.8299989 +0800 CST m=+1.005164601
Tick at 2021-06-20 20:44:11.8299812 +0800 CST m=+2.005146901
Tick at 2021-06-20 20:44:12.8312571 +0800 CST m=+3.006422801
Tick at 2021-06-20 20:44:13.8298241 +0800 CST m=+4.004989801
Tick at 2021-06-20 20:44:14.8309582 +0800 CST m=+5.006123901
Tick at 2021-06-20 20:44:15.8302667 +0800 CST m=+6.005432401
Tick at 2021-06-20 20:44:16.8301526 +0800 CST m=+7.005318301
Tick at 2021-06-20 20:44:17.8297111 +0800 CST m=+8.004876801
Tick at 2021-06-20 20:44:18.8301434 +0800 CST m=+9.005309101
Ticker stopped