Go语言通道

函数单纯并发执行是没有意义的,函数与函数之间交换数据才可以体现并发执行函数的意义,其中Go语言的并发模型是csp模型,提倡通过通信共享内存的方式而不是通过共享内存而实现通信,共享内存实现通信在不同的goroutine中容易发生竞态的问题,为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,但是这种做法会造成性能上的一些问题。在go语言中,goroutine是go程序并发的执行体,go语言为goroutine之间的通信提供了一个桥梁:channel,也即goroutine之间的连接,channel是一种能够让一个goroutine发送特定值到另外一个goroutine的通信机制,属于一种特殊的类型,有点像队列,遵循先进先出的规则,保证数据收发的顺序,每一个通道都有其特定的类型,在声明的时候需要指定元素类型(所以一般通道会结合go关键字启动goroutine使用)。

一. 声明channel

类似于其他的基本数据类型,可以使用var关键字来声明,语法规则:var 变量 chan 元素类型,通道属于引用类型,通道类型的空值为nil,声明通道之后需要使用make函数初始化才可以使用,使用make函数创建通道的格式:make(chan 元素类型,[缓冲大小]),缓冲大小是可选的:

package main

import (
	"fmt"
)

func main() {
	// 声明的通道需要make函数初始化之后才可以使用
	var a chan int
	a = make(chan int)
	b := make(chan bool)
	c := make(chan []int)
	fmt.Println(a, b, c)
}

二. channel操作

通道有发送接收关闭三种操作,发送和接收使用符号 "<-",在关闭通道的时候需要注意只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道,通道可以被垃圾回收机制回收,与关闭文件是不一样的,在结束操作之后关闭文件是必须要做的事情,但是关闭通道不是必须要做的事情;关闭后的通道具有几个特点:① 当通道关闭之后再发送值就会导致panic;② 对于关闭的通道进行接收会一直获取值直到通道为空(关闭后可以读取);③ 对于一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值;④ 关闭已经关闭的通道会导致panic;可以使用for-range的方式来遍历通道,但是需要注意两个问题:① 在遍历时,如果通道没有关闭,则会出现deadlock的错误;② 在遍历时,如果通道已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。下面是一个通道发送和接收数据例子,这个例子会出现死锁,在main()函数中向通道a写入数据,由于是无缓冲的通道所以一定需要有人来接收数据,执行到a <- 8的时候等待另外一个goroutine来接收通道的数据,此时不会继续往下执行语句,由于没有其他的goroutine接收通道的数据所以main()函数会一直等待就出现了死锁的问题(main函数出现了阻塞就会发生死锁的问题):

package main

import "fmt"

func main() {
	// 声明的通道需要make函数初始化之后才可以使用, 可以使用简短声明
	var a chan int
	a = make(chan int)
	// 将8发送到通道中
	a <- 8
	// 从一个通道中接收值
	x := <-a // 从通道a中接受值并且赋值给x
	fmt.Println(x)
	// 关闭通道
	close(a)
}

三. 无缓冲的通道

无缓冲的通道又称为阻塞的通道,下面的代码编译可以通过的但是运行的时候会显示所有的goroutine都阻塞出现死锁的情况,其实我们使用a := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值,也即无缓冲的通道必须有接收才能发送,如何解决这个问题呢?我们可以使用另外一个goroutine去接收值,所以对于无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行,所以使用无缓冲通道进行通信的时候将导致发送和接收的goroutine同步化,并且需要注意的是下面这个例子中代码顺序为:ch <- 8  go receive(ch)的时候会出现死锁的问题,因为main()函数中的语句从上到下执行,执行到ch <- 8这一句的时候会等待另外一个goroutine来接收数据,此时不会往下执行,由于没有往下执行go receive(ch)所以没有一个goroutine来接收数据所以会出现阻塞的情况,main()函数一旦出现阻塞那么就会出现死锁的问题,正确的顺序为 go receive(ch)   ch <- 8 ,由于语句从上往下执行所以先执行 go receive(ch),此时等待其他的goroutine向ch写入数据,当切换到main()函数的主协程的时候此时向ch写入数据,那么当切换到receive()函数所在协程的时候从ch中取出数据此时就完成了main()函数的协程和receive()函数的协程的并发操作,此时不会发生死锁的问题,所以很多时候使用go关键字开启一个goroutine并且和通道发送和接收数据结合的时候需要注意调用的顺序,这个时候很容易发生死锁的问题:

package main

import (
	"fmt"
	"time"
)

// 开启另外一个goroutine从通道上接收值
func receive(c chan int) {
	x := <-c
	fmt.Println(x)
}

func main() {
	ch := make(chan int)
	// 开启另外一个goroutine从通道上接收值
	go receive(ch)
	// 将8发送到通道中
	ch <- 8
	// 关闭通道
	close(ch)
	// 让main中的主协程等待1s使得goroutine可以执行完
	time.Sleep(time.Second)
}

错误的代码语句顺序(发生死锁):

package main

import (
	"fmt"
	"time"
)

// 开启另外一个goroutine从通道上接收值
func receive(c chan int) {
	x := <-c
	fmt.Println(x)
}

func main() {
	ch := make(chan int)

	// 将8发送到通道中, 执行到下面这一句的时候会等待其他的goroutine接收数据所以不会往下执行, 一直等待所以main函数就出现了死锁问题
	ch <- 8

	// 开启另外一个goroutine从通道上接收值
	go receive(ch)
	
	// 关闭通道
	close(ch)
	// 让main中的主协程等待1s使得goroutine可以执行完
	time.Sleep(time.Second)
}

下面的例子中使用go关键字开启了goroutine向通道发送和接收数据的例子,使用go关键字调用这两个函数的顺序是可以随意的因为从哪一个函数执行都不会发生死锁的问题,通道会正常发送和接收数据:

package main

import (
	"fmt"
	"time"
)

func receive(c chan int) {
	defer close(c)
	x := <-c
	fmt.Println(x)
}

func send(c chan int, num int) {
	c <- num
}
func main() {
	ch := make(chan int)

	// 将8发送到通道中(接收和发送两个goroutine顺序可以随意, 而两个函数是并发执行的, 由go语言的调度机制先执行哪一个goroutine都不会发生一直等待的问题)
	go send(ch, 8)

	// 开启另外一个goroutine从通道上接收值
	go receive(ch)
	
	// 让main中的主协程等待1s使得两个goroutine可以执行完
	time.Sleep(time.Second)
}

四. 有缓冲的通道

我们只需要在make函数初始化通道的时候指定通道的大小,当通道的容量大于0之后那么就是有缓冲的通道,通道的容量表示通道中存放元素的数量,当通道元素数量大于容量之后就会出现阻塞,下面是从往通道中发送至和通道中取值的例子:

package main

import "fmt"

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	// 开启goroutine将0~50的数发送到ch1中, 匿名函数调用
	go func() {
		for i := 0; i < 50; i++ {
			ch1 <- i
		}
		close(ch1)
	}()
	// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中, 匿名函数调用
	go func() {
		for {
			i, ok := <-ch1
			if !ok {
				break
			}
			ch2 <- i * i
		}
		close(ch2)
	}()
	// 在主goroutine中从ch2中接收值打印
	for i := range ch2 { // 通道关闭后会退出for range循环
		fmt.Println(i)
	}
}

五. 单向通道

有些时候我们会将通道作为参数在不同的goroutine中进行传递,对于不同的任务中使用通道有不同的限制,go语言提供了单向通道来限制通道在函数中只能够发送或者接收,例如chan <- int是一个只能发送的通道,<- chan int是一个只能够接受的通道。

六. 通道容易发生的死锁问题

在编写多个通道启动goroutine来执行任务的时候特别容易发生死锁的问题,main()函数属于主协程,执行语句是从上到下执行的,例如某个go程序有三个协程,其中一个是main()函数对应的主协程,另外两个是使用go关键字启动的goroutine,这三个协程是并发执行的,可能某个goroutine执行到某条语句的时候会切换到另外一个goroutine执行,如果启动了goroutine执行某个任务的过程中,向通道发送了数据那么就一定需要有人来接收通道数据(需要使用通道来接收数据),如果没有其他的goroutine来接收数据或者是两个goroutine相互等待就会发生死锁的问题,下面的例子就会产生两个goroutine相互等待出现阻塞的问题,不管是函数f1()还是f2()先执行,两个goroutine都会出现阻塞相互等待的问题,但是当main()函数所在的主协程结束的时候那么两个goroutine也会一起结束即使两个goroutine出现了阻塞,这是因为使用go关键字开启了另外的协程导致的阻塞问题但是主协程没有发生阻塞那么两个goroutine也会跟随主协程一起结束最终会结束掉整个程序,f1()函数一开始往通道ch2发送数据,f2()函数中从通道ch1取数据,但是通道ch1没有数据所以等待协程f1往ch1发送数据,等待的时候f2()函数不会往下执行,协程f1则等待协程f2()从通道ch2取数据,这样就造成了相互等待所以出现了阻塞的问题:

package main

import (
	"fmt"
	"time"
)

func f1(ch1, ch2 chan string) {
	ch2 <- "ch2"
	ch1 <- "ch1"
}

func f2(ch1, ch2 chan string) {
    // 如果先执行f2, 执行到<-ch1的时候ch1没有数据则等待其他的协程是否往ch1发送数据, 这个时候不会往下执行<-ch2语句, 而切换到f1所在的goroutine的时候也会出现需要向ch2取数据的情况这样两者相互等待出现了阻塞
	fmt.Println(<-ch1)
	fmt.Println(<-ch2)
}

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)
    // 使用了go启动了另外的协程所以即使goroutine发生了阻塞最终也会跟随主协程结束, 不会出现死锁的问题
	go f1(ch1, ch2)
	go f2(ch1, ch2)
    
	time.Sleep(time.Second * 4)
}

如果我们调换一下顺序f1和f2两个goroutine就不会出现阻塞的问题,因为这两个协程是并发执行的不会出现等待的问题,不管是先执行哪个goroutine都不会发生阻塞:

package main

import (
	"fmt"
	"time"
)

func f1(ch1, ch2 chan string) {
	ch1 <- "ch1"
	ch2 <- "ch2"
}

func f2(ch1, ch2 chan string) {
	fmt.Println(<-ch1)
	fmt.Println(<-ch2)
}

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)
    // 使用go关键字开启协程两个协程并发执行不会出现相互等待的情况所以正常执行
	go f1(ch1, ch2)
	go f2(ch1, ch2)

	time.Sleep(time.Second * 4)
}

我们去除掉使用go关键字开启goroutine的调用可以发现也会出现死锁的问题,main()函数的语句是从上往下执行,如果main()函数的主协程发生了某个通道等待发送数据或者接收数据的时候就会出现死锁的情况,运行的时候会判断main()函数所在的主协程是否等待,如果出现一直等待的情况则判定为死锁:

package main

import (
	"fmt"
	"time"
)

func f1(ch1, ch2 chan string) {
	ch1 <- "ch1"
	ch2 <- "ch2"
}

func f2(ch1, ch2 chan string) {
	fmt.Println(<-ch1)
	fmt.Println(<-ch2)
}

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)
    // 由于是从上往下执行的, 所以ch1和ch2会等待其他的goroutine接收数据但是并没有其他通道来接收数据所以会出现死锁, 此时等待的过程不会执行f2
	f1(ch1, ch2)
	f2(ch1, ch2)

	time.Sleep(time.Second * 4)
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值