go语言提高(二):goroutine、runtime包、channel、定时器
1. go程 goroutine
1.1 go程的特性
-
创建:在函数调用的前面添加关键字go关键字。创建go程
-
特性:
- 主go程先于子go程结束运行,自动释放进程的地址空间,go程也就被动的退出了。
-
举例:
注意:创建go程后主函数变为主go程,当主go程退出后会释放整个进程地址空间,进程就退出了,其他的go程也就直接退出了。
func singing() {
for i:=0; i<5; i++{
fmt.Println("--- 正在唱歌:人猿泰山 ---")
time.Sleep(time.Microsecond* 300)
}
}
func dangcing() {
for i:=0; i<5; i++{
fmt.Println("--- 正在跳舞:小苹果~ ---")
time.Sleep(time.Microsecond* 300)
}
}
func main() {
// 不能将所有的函数全作为go程,如果这样的话主go程结束后会释放进程地址空间,直接结束整个进程
go singing()
dangcing()
}
1.2 go程的创建
func test() {
for i := 0; i < 5; i++ {
fmt.Println("实名go程, hello", i+1)
time.Sleep(time.Millisecond * 20)
}
}
func main() {
go test()
go func() {
for i := 0; i < 5; i++ {
fmt.Println("匿名go程, hello", i+1)
time.Sleep(time.Millisecond * 20)
}
}()
for i := 0; i < 5; i++ {
fmt.Println("主go程, hello", i+1)
time.Sleep(time.Millisecond * 20)
}
time.Sleep(time.Second * 3)
}
1.3 创建N个go程
func test1(i *int) {
fmt.Println("这时创建的第", *(i)+1, "个go程")
}
func main() {
for i:=0; i<10; i++ {
go test1(&i)
}
time.Sleep(time.Second * 2)
}
// 打印结果
这时创建的第 11 个go程
这时创建的第 11 个go程
这时创建的第 11 个go程
这时创建的第 11 个go程
这时创建的第 11 个go程
这时创建的第 11 个go程
这时创建的第 11 个go程
这时创建的第 11 个go程
这时创建的第 5 个go程
这时创建的第 11 个go程
- 原因:在主go程中创建子go程的速度很快,但是子go程抢夺到cpu进行打印输出却抢夺不到,所以传进去地址的话,子go程内的i是外部主go程中的i的值,在已经创建了很多个go程并且i已经累加了很多时,子go程才抢夺到cpu进行屏幕打印,所以打印的值基本都是i循环结束后的值,可能会有个别的子go程抢夺到的较早而打印了一个较小的值。
1.4 go程的退出
-
return:返回当前函数调用, defer有效
-
runtime.Goexit():终止调用该函数的go程,终止go程前会调用所有的defer的延迟调用函数
- 在程序的main go程调用本函数,会终结该go程,而不会让main返回。因为main函数没有返回,程序会继续执行其它的go程。如果所有其它go程都退出了,程序就会崩溃。
- 也就是在main函数中调runtime.Goexit(),程序就会在所有go程结束后崩溃 —deadlock错误。
-
os.Exit():需要传递一个参数,表示退出值。用来终止调用该函数的进程,并且defer无效。
- ps:go语言中的死循环
for {
;
}
2. runtime包简介
- runtime.Goshed():Gosched使当前go程放弃处理器,以让其它go程运行。它不会挂起当前go程,因此当前go程未来会恢复执行。
- runtime.GOROOT():GOROOT返回Go的根目录。如果存在GOROOT环境变量,返回该变量的值;否则,返回创建Go时的根目录。
Ps: os.Stdin.Read(str),可以从键盘读取带空格的数据
3. channel
go语言中的channel类似于管道
3.1 channel的定义
-
定义语法:
var ch = make(chan 通道中传递的数据类型, 容量大小)
- 例如:
ch := make(chan int)
- 例如:
ch := make(chan string, 0)
- 例如:
-
读写:
- 读:
<-ch
读到数据丢弃num := <-ch
读到数据,存入num中
- 写:
ch<- data
data类型严格与定义的语法一致
- 读:
-
特性:
- 通道中的数据只能单向流动。一端读端、另外必须写端。
- 通道中的数据只能一次读取,不能重复读。
- 读端和写端在不同的goroutine之间。
- 读端读,写端不在线,读端阻塞。写端写,读端不在线,写端阻塞。
- 数据是先进先出。
// 创建一个channel通道,用于实现同步
var ch = make(chan int) // 必须要使用make,不能只定义不make,make相当于初始化
func printer(str string) {
for _, chr := range str {
fmt.Printf("%c", chr)
time.Sleep(time.Millisecond * 300)
}
}
func user1() { // 先使用printer
printer("hello")
// 写channel操作,在写之前,对端如果读的话就会一直阻塞,直到本端写了数据为止
ch <- 10
}
func user2() { // 后使用printer
// 读channel操作,在对端写之前,此端阻塞。
<- ch
printer("world")
}
func main() {
go user1()
go user2()
for {
;
}
}
3.2 channel实现同步
// 创建一个channel通道,用于实现同步
var ch = make(chan int) // 必须要使用make,不能只定义不make,make相当于初始化
func printer(str string) {
for _, chr := range str {
fmt.Printf("%c", chr)
time.Sleep(time.Millisecond * 300)
}
}
func user1() { // 先使用printer
printer("hello")
// 写channel操作,在写之前,对端如果读的话就会一直阻塞,直到本端写了数据为止
ch <- 10
}
func user2() { // 后使用printer
// 读channel操作,在对端写之前,此端阻塞。
<-ch
printer("world")
}
func main() {
go user1()
go user2()
for {
;
}
}
3.3 channel传递数据
func main() {
var ch = make(chan int) // 用来控制通信
var ch1 = make(chan bool) // 用来控制stdout的同步
go func() {
for i:=0; i<3; i++ {
ch <- i
fmt.Printf("子go程写%d给主go程\n", i) // stdout
ch1 <- false
}
}()
for i:=0; i<3; i++ {
num := <- ch
<- ch1
fmt.Println("主go程读到:", num) // stdout
}
}
程序中使用了两个channel,其中一个用于go程间的数据的通信,另一个用于控制go程间的标准输出的同步。写channel那块代码先执行,读channel那块代码后执行。
使用channel协调了先后顺序。
3.4 channel的分类
无缓冲channel
无缓冲channel要求两端必须在线才行,关键字是阻塞和同步
- 定义时capacity写0,或者不写capacity表示无缓冲channel
- 在无缓冲channel的使用过程中,len和cap一直是0
有缓冲channel
有缓冲channel在定义时要指定capacity,不需要两端同时在线,读端不在线时写端也可以写,前提条件是没有将channel写满。如果channel已经写满,那么再写的话也会阻塞。
使用len(chan)可以查看里面的数据有多少个,cap(chan)可以查看它的容量大小
3.5 同步通信和异步通信
- 同步通信: – 无缓冲channel
-
- 一个调用发出,如果没有得到结果,那么该调用不返回。 – 阻塞
- 相当于打电话,双方必须同时在线
- 异步通信: – 有缓冲channel
- 一个调用发出,不等待结果,直接返回。 – 不阻塞。
- 相当于发短信。双方不需要同时在线。一端发送完,立即返回。
3.6 channel的关闭
close(channel名)
即可关闭
判断是否关闭,使用从channel读数据时的bool类型的返回值来判断,如果是false表示管道已关闭,true表示还没有关闭。
func main() {
ch := make(chan int)
go func() {
for i:=0; i<10; i++ {
ch <- i // 写channel
if i == 5 {
close(ch)
runtime.Goexit()
}
}
}()
for i:=0; i<10; i++ {
if data, ok := <- ch; ok { // 判断对端是否关闭,如果关闭ok == false
fmt.Println(data)
} else {
fmt.Println("channel关闭")
break
}
}
}
- 判断对端是否关闭的简便写法
// 读channel时可以使用for-range,当对端关闭时直接跳出循环
for data := range ch {
fmt.Println(data)
}
-
关闭channel的特性:
-
如果写端没有关闭,暂停写入,读端阻塞等待
-
如果写端已经关闭,不能写入数据。
报错:panic: send on closed channel
-
如果写端已经关闭,读端依然能读取,读到的是数据类型零值(默认值)。
-
3.7 单向channel
默认channel是双向的,可以定义一个单向channel
单向channel的定义:
var ch2 chan<- float64 // ch2是单向channel,只用于写float64数据
var ch3 <-chan int // ch3是单向channel,只用于读int数据
-
可以将双向channel隐式的给单向channel赋值, 但是不能将单向channel转换为双向channel
-
单向读channel只能读不能写,如果写的话编译会出错。
-
单向写channel只能写不能读,如果读的话编译器的语句检查即报错。
func main() {
// 定义一个双向channel
ch := make(chan int)
var chr <-chan int = ch // 使用双向channel来给单向读channel初始化
<-chr
var chw chan<- int = ch // 使用双向channel来给单向读channel初始化
chw <- 10
}
Ps:以上代码编译正确
- 单向channel的应用
- 主要用于函数调用的传参。单向channel可以在语法方面对函数的操作进行限制。
- 单向读:不能进行写操作
- 单向写:不能进行读操作
4. 生产者消费者模型
没什么可说的,太经典的模型了,注意要使用公共缓冲区来实现解耦和并发。
- 一个比较简单的单生产者单消费者模型
func producer(send chan<- int) {
for i := 0; i < 10; i++ {
send <- i
fmt.Println("生产者生产了", i)
}
close(send)
}
func consumer(recv <-chan int) {
for data := range recv {
fmt.Println("消费者消费了", data)
}
}
func main() {
ch := make(chan int, 5)
// 生产者生产
go producer(ch)
// 消费者消费
consumer(ch)
}
5. 定时器Timer
5.1 单次定时
func NewTimer(d Duration) *Timer
type Duration int64
type Timer struct {
C <-chan Time
// 内含隐藏或非导出字段
}
-
单次定时的使用步骤:
- 使用time.NewTimer() 函数,指定定时的时长
- 读C管道,到达时间后会解除阻塞,返回一个系统当前时间。
-
time.After()函数,参数传定时时间,直接读time.After()函数即可。使用起来比较方便。
-
func After(d Duration) <-chan Time
-
func main() {
// 设置单次定时的时长,返回值是一个结构体指针
timer := time.NewTimer(time.Second * 3)
fmt.Println(time.Now())
// 从timer中进行读操作
// 在定时期间会阻塞,当定时时长结束时系统会将当前时间写入到C中,即在此处读出
t := <-timer.C
fmt.Println(t)
t = <-time.After(time.Second * 3)
fmt.Println(t)
}
5.2 停止定时器
func (t *Timer) Stop() bool
Stop不会关闭通道t.C。
5.3 重置定时器
func (t *Timer) Reset(d Duration) bool
将定时器的定时时长定为重置的该时间.
如果调用时t还在等待中会返回真;如果t已经到期或者被停止了会返回假。
5.4 周期定时器Ticker
func NewTicker(d Duration) *Ticker
每过设定好的秒数系统就会向timer的C管道中写入当期的系统时间。
-
Ticker对象的函数只有Stop()函数停止计时,没有Reset()函数重置计时和After()函数
-
所以只能在创建它的时候指定它的定时时长,不能在中间改变它的定时时长。