Golang ——并发编程

goroutine

goroutine是Go语言中的轻量级线程实现,由Go运行时管理,其使用方式也特别简单,只需要加上go关键字就可以了。

func StudyGoroutine() {

	go newTask() // 使用go关键字,新建一个协程

	for {
		fmt.Println("this is test1 goroutine")
		time.Sleep(time.Second) // 延时1s
	}
}

func newTask() {
	for {
		fmt.Println("this is newTask goroutine")
		time.Sleep(time.Second) // 延时1s
	}
}

runtime.Gosched() —— 让出时间片

  • runtime.Gosched()用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
  • 类似于一场接力赛,A跑了一段时间碰到代码runtime.Gosched(),然后就把接力棒交给B了,A歇着了,B继续跑。
func StudyGosched() {

	go func() {
		for i := 0; i < 5; i++ {
			fmt.Println("go")
		}
	}()

	for i := 0; i < 2; i++ {
		// 让出时间片,先让别的协程执行,它执行完,再回来执行此协程
		runtime.Gosched()
		fmt.Println("hello")
	}
}

runtime.Goexit() —— 终止所在的协程

  • runtime.Goexit()会立即终止当前的goroutine执行,调度器确保所有已经注册的defer延迟调用执行
func StudyGoexit() {

	// 创建一个新的协程
	go func() {
		fmt.Println("aaaaaaaaa")

		// 调用别的函数
		test()

		fmt.Println("bbbbbbbbbbbb")
	}()

	// 这里写一个死循环,目的不让主协程结束
	for {
	}
}

func test() {
	defer fmt.Println("ccccccccccccc")

	//return // 终止此函数,
	//打印 aaaaaaaa   ccccccccc  bbbbbbbb
	//下面打印的 dddddddddd 不再打印,因为执行不到下面

	runtime.Goexit() // 终止所在的协程
	// 当执行到这一句时,当前协程会被终止,dddddd和bbbbbb 都不会打印
	// 最终打印 aaaaaaa  cccccccc

	fmt.Println("dddddddddddddd")
}

runtime.GOMAXPROCS() —— 设置CPU核数

  • runtime.GOMAXPROCS()用来设置可以并行计算的CPU核数的最大值,并返回之前的值
func StudyGOMAXPROCS() {
	n := runtime.GOMAXPROCS(1) // 指定以单核运算
	fmt.Println(n)
	for {
		go fmt.Println(1)

		fmt.Println(2)
		// 当单核运算时,只能连续打印11111 或者连续打印22222,隔很长之后才会交替打印
		// 当多核运算时,打印1和2之间的间隔会比单核小很多
	}
}

channel

  • channel是Go语言在语言级别提供的goroutine间的通信方式。我们可以使用channel在两个或者多个goroutine之间传递消息。
  • channel是进程间的通信方式,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等

基本语法

  • 一般channel的声明格式为 var chanName chan ElementType。与一般的变量不同的地方仅仅在类型之前加了chan关键字。ElementType指定这个channel能传递的元素类型。
  • 定义一个channel也很简单,直接使用内置make()使用即可。
    ch := make(chan int) 这就声明并且初始化了一个int型的名为ch的channel
  • 在channel的用法中,最常见的就是写入和读出了。
    • 将一个数据写入channel:ch <- value,向channel写入数据通常会造成阻塞,直到有其他goroutine从这个channel中读取数据;
    • 从channel中读取数据的语法是value := <- ch,如果channel之前没有写入数据,那么从channel中读取数据也会导致阻塞,直到channel中被写入数据为止。

代码实践一下channel

func test1() {
	// 新建两个协程,代表2个人,2个人同时使用打印机
	 go person1()
	 go person2()

	 // 为了不让主协程结束,来一个死循环
	 // 不进行channel相关操作的时候,
	 //打印结果是 “hwoelrllod” 也就是hello world 这两个协程随机获取到时间片,然后一个字符一个字符的打印
	for  {
	}
}

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

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

func Printer(s string) {
	for _, data := range s {
		fmt.Printf("%c", data)
		time.Sleep(time.Second)
	}
	fmt.Printf("\n")
}

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

func test2() {

	// 创建channel
	ch1 := make(chan string)

	defer fmt.Println("主协程也结束")

	go func() {
		defer fmt.Println("子协程调用完毕")

		for i := 0; i < 2; i++ {
			fmt.Println("子协程 i = ", i)

			time.Sleep(time.Second)
		}
		ch1<- "我是子协程,我工作完毕"
	}()

	str := <-ch1 // 没有数据前,会阻塞
	fmt.Println("str = ", str)
}

缓冲机制

之前示范创建使用的channel都是无缓冲的channel,这种对于传递单个数据的场景可以接受,但是如果是需要持续传输大量数据的场景就不合适了。

func test3() {
	// 创建一个无缓冲的channel
	ch2 := make(chan int)

	// len(ch2)表示缓冲区剩余数据的个数,cap(ch2)表示缓冲区大小
	fmt.Printf("len(ch2) = %d, cap(ch2) = %d\n", len(ch2), cap(ch2))
	// 新建协程
	go func() {
		for i := 0; i < 3; i++ {
			fmt.Println("子协程i = ", i)
			ch2 <- i // 往channel写内容
		}
	}()
	// 延时
	time.Sleep(2 * time.Second)

	for i := 0; i < 3; i++ {
		num := <- ch2 // 读channel中的内容,没有内容前会阻塞
		fmt.Println("num = ", num)
	}
}

下面,就看看怎么给channel带上缓冲,从而达到消息队列的效果:

c := make(chan int, 1024)

在调用make()函数的时候,将缓冲区大小作为第二个参数传入,比如上面这个例子就创建了一个大小为1024的int类型的channel。在带有缓冲的channel时,即使没有读取方,写入方也可以一直往channel中写入,在缓冲区被填满之前都不会阻塞

func test4() {
	// 创建一个有缓冲的channel
	ch2 := make(chan int, 3)
	// len(ch2)表示缓冲区剩余数据的个数,cap(ch2)表示缓冲区大小
	fmt.Printf("len(ch2) = %d, cap(ch2) = %d\n", len(ch2), cap(ch2))
	// 新建协程
	go func() {
		for i := 0; i < 10; i++ {
			ch2 <- i // 往channel写内容
			fmt.Printf("子协程[%d]: len(ch2) = %d, cap(ch2) = %d\n", i, len(ch2), cap(ch2))
		}
	}()
	// 延时
	time.Sleep(2 * time.Second)

	for i := 0; i < 10; i++ {
		num := <- ch2 // 读channel中的内容,没有内容前会阻塞
		fmt.Println("num = ", num)
	}
}

channel的关闭

  1. channel不像文件一样,需要经常去关闭,只有当你确认没有任何数据发送了,或者你想显示的结束range循环之类的,才会去关闭channel
  2. 关闭channel后,无法向channel再发送数据(引发panic错误后导致接收立即返回零值)
  3. 关闭channel后,可以继续向channel接收数据
  4. 对于nil channel,无论收发都会被阻塞
func test5() {

	ch2 := make(chan int)// 创建一个无缓冲的channel

	// 新建一个goroutine
	go func() {
		for i := 0; i < 5; i++ {
			ch2 <- i // 往通道里写数据
		}
		// 当不需要写数据时,关闭通道
		close(ch2)
	}()

	for true {
		if num, ok := <-ch2; ok == true {
			// 如果ok == true,那就说明channel还没有关闭
			fmt.Println("num = ", num)
		} else {
			fmt.Println("通道关闭")
			break
		}
	}

	// 相比较上面这样遍历channel,有一种更简单的遍历方法
	for num1 := range ch2 {
		fmt.Println("num1 = ", num1)
	}

}

单向channel

  • 默认情况下,channel是双向的,可以读数据,也可以进行写数据
  • 单向channel只能进行写或者读:chan <- 表示进入管道,也就是写数据; <- chan 表示从管道出来,也就是读数据

func test6() {

	var ch1 chan int // 普通双向channel
	var ch2 <- chan int // 只能读数据的channel
	var ch3 chan <- int // 只能写数据的channel
	var ch4 chan <- int = ch1 // 双向channel可以隐式的转换为单向channel,单向无法转化为双向

	 <- ch2
	 ch3 <- 666
 	 ch4 <- 777


}

单向channel的应用

func test7() {
	// 创建一个双向channel
	ch2 := make(chan int)

	// 生产者,生产数字,写入channel
	// 新创建一个协程
	go producer(ch2) // channel传参,属于引用传递

	// 消费者,从channel读取内容,打印
	consumer(ch2)
}

// 这个函数只用读,不用写
func consumer(in <- chan int) {
	for num := range in {
		fmt.Println("num = ", num)
	}
}

// 这个函数只用写,不用读,所以使用单向channel
func producer(out chan <- int) {
	for i := 0; i < 10; i++ {
		out <- i * i
	}
	close(out)
}

定时器Timer和Ticker

Timer

  • 代表未来的一个单一事件,你可以告诉Timer你要等待多长时间,它提供一个channel,在将来的那个时间那个channel提供一个时间值。
  • 也就是Timer在设置的时间到了之后,只会响应一次。
func LearnTimer() {
	// 创建一个定时器,设置时间2s,2s后,它会往channel中写内容(当前时间)
	timer := time.NewTimer(2*time.Second)
	fmt.Println("当前时间:", time.Now())

	// 2s过后,往timer.C写数据,有数据后,就可以读取了,没有数据前会阻塞
	t := <- timer.C
	fmt.Println("t = ", t)
}

通过Timer实现延时功能
func testTimer() {
	// 延时2s后打印一句话

	// 1. 通过Timer实现
	timer := time.NewTimer(2 * time.Second)
	<-timer.C
	fmt.Println("时间到")

	// 2,通过Sleep实现
	time.Sleep(2 * time.Second)
	fmt.Println("时间到")

	// 3. 通过After实现
	<-time.After(2 * time.Second) // 定时2s,阻塞2s,2s后产生一个事件,往channel中写内容
	fmt.Println("时间到")
}
停止定时器
func StopTimer() {
	timer := time.NewTimer(3 * time.Second)

	go func() {
		<-timer.C // 定时器停止之后,这句就无效了
		fmt.Println("子协程可以打印了,因为定时器的时间到了")
	}()

	timer.Stop() //停止定时器

	for true {
		fmt.Println("主协程")
	}
}
重置定时器
func ResetTimer() {
	// 将3s重置为2s
	timer := time.NewTimer(3 * time.Second)
	ok := timer.Reset(2 * time.Second)
	if ok == true {
		<- timer.C
		fmt.Println("2s时间到")
	} else {
		<-timer.C
		fmt.Println("3s时间到")
	}
}

Ticker

  • Ticker是一个定时触发的计时器,它会以一个间隔往channel发送一个事件(当前时间),而channel的接收者可以以固定时间间隔从channel中读取事件。
  • 其实就是像上闹钟一样,循环周期性发送事件。
func LearnTicker() {

	ticker := time.NewTicker(2 * time.Second)
	
	i := 0
	for {
		<- ticker.C // 设置没2s打印一次

		i++
		fmt.Println("i = ", i)

		// 设置5s后停止
		if i == 5 {
			ticker.Stop()
			break
		}
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值