go语言提高(二):goroutine、runtime包、channel、定时器

1 篇文章 0 订阅
1 篇文章 0 订阅

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)
}

// 打印结果
这时创建的第 11go程
这时创建的第 11go程
这时创建的第 11go程
这时创建的第 11go程
这时创建的第 11go程
这时创建的第 11go程
这时创建的第 11go程
这时创建的第 11go程
这时创建的第 5go程
这时创建的第 11go
  • 原因:在主go程中创建子go程的速度很快,但是子go程抢夺到cpu进行打印输出却抢夺不到,所以传进去地址的话,子go程内的i是外部主go程中的i的值,在已经创建了很多个go程并且i已经累加了很多时,子go程才抢夺到cpu进行屏幕打印,所以打印的值基本都是i循环结束后的值,可能会有个别的子go程抢夺到的较早而打印了一个较小的值。

1.4 go程的退出

  1. return:返回当前函数调用, defer有效

  2. runtime.Goexit():终止调用该函数的go程,终止go程前会调用所有的defer的延迟调用函数

    • 在程序的main go程调用本函数,会终结该go程,而不会让main返回。因为main函数没有返回,程序会继续执行其它的go程。如果所有其它go程都退出了,程序就会崩溃。
    • 也就是在main函数中调runtime.Goexit(),程序就会在所有go程结束后崩溃 —deadlock错误。
  3. os.Exit():需要传递一个参数,表示退出值。用来终止调用该函数的进程,并且defer无效。

  • ps:go语言中的死循环
for {
    ;
}

2. runtime包简介

  1. runtime.Goshed():Gosched使当前go程放弃处理器,以让其它go程运行。它不会挂起当前go程,因此当前go程未来会恢复执行。
  2. 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类型严格与定义的语法一致
  • 特性:

    1. 通道中的数据只能单向流动。一端读端、另外必须写端。
    2. 通道中的数据只能一次读取,不能重复读。
    3. 读端和写端在不同的goroutine之间。
    4. 读端读,写端不在线,读端阻塞。写端写,读端不在线,写端阻塞。
    5. 数据是先进先出。
// 创建一个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()函数

  • 所以只能在创建它的时候指定它的定时时长,不能在中间改变它的定时时长。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值