首页 > 其他分享 >go语言并发-02channel

go语言并发-02channel

时间:2022-08-23 09:13:46浏览次数:48  
标签:02channel 缓冲 goroutine 阻塞 并发 go 接收 channel 通道

go语言通道channel

  1. 如果说 goroutine 是 Go语言程序的并发体的话,那么 channels 就是它们之间的通信机制。一个 channels 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。
    go语言提倡使用通信的方法代替共享内存,当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。
  • 多个goroutine为了争抢数据,势必造成执行的低效率,使用队列的方式是最高效的,channel就是一种队列一样的结构
  1. 通道的特性
    go语言中的通道(channel)是一种特殊的类型,在任何时候,同时只能有一个goroutine访问通道并进行发送和获取数据,goroutine间通过通道就可以通信,通道就像一个传送带或者队列,总是遵循先入先出的规则,保证收发数据的顺序。
  2. 使用通道接收数据
    3.1 无缓冲通道的收发操作在两个不同的goroutine间进行
    由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,直到发送方发送数据位置。
    3.2 接收将持续阻塞直到发送方发送数据
    如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止
    3.3 每次接收一个元素
    通道一次只能接收一个数据元素
    通道的数据接收一共有一下4中写法
    (1)阻塞接收数据
    阻塞模式接收数据时,将接收变量作为<-操作符的左值:
    data := <- ch
    执行该语句时将会阻塞,直到接收到数据并赋值给data变量。
    (2)非阻塞接收数据
    使用非阻塞方式从通道接收数据时,语句不会发生阻塞:
    data, ok := <- ch
    data:表示接收到的数据,未接收到数据是返回通道类型的零值。
    ok:表示是否接收到数据。
    非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel 进行,可以参见后面的内容。
    (3)接收任意数据,忽略接收的数据
    阻塞接收数据时,忽略从通道接收到的数据:
    <-ch
    执行该语句将会发生阻塞,直到接收到数据,但接收到的数据会被忽略,这个方式实际只是通过通道在goroutine间阻塞收发实现并发同步。
  • 使用通道做并发同步的案例:
func main() {
	// 使用通道做并发的同步
	ch := make(chan int)
	go func() {
		fmt.Println("start goroutine")
		ch <- 0
		fmt.Println("exit goroutine")
	}()
	fmt.Println("wait goroutine")
	<-ch
	fmt.Println("all done")
}

执行结果

start goroutine
wait goroutine
all done
或者
wait goroutine
start goroutine
exit goroutine 
all done

代码说明:
构建一个 无缓冲/同步 通道;
开启一个匿名函数的并发;
匿名goroutine即将结束时,通过通道通知main goroutine,这一句会一直阻塞直到main goroutine接收到为止。
(4)循环接收
通道的数据接收可以借用for range语句进行多个元素的接收操作:
通道 ch 是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过 for 遍历获得的变量只有一个,即上面例子中的 data。
使用for从通道中接收数据案例:

func main() {
	ch := make(chan int)
	go func() {
		for i := 3; i >= 0; i-- {
			ch <- i
			time.Sleep(time.Second)
		}
		close(ch)
	}()
	for data := range ch {  // 如果通道关闭了,此处也就跳出循环了
		fmt.Println(data)
	}
}

并发打印

前面的例子创建的都是无缓冲通道。使用无缓冲通道往里面装入数据时,装入方将被阻塞,直到另外通道在另外一个 goroutine 中被取出。同样,如果通道中没有放入任何数据,接收方试图从通道中获取数据时,同样也是阻塞。发送和接收的操作是同步完成的。

  • 无缓冲通道发送方和接收方的操作是同步完成的
    并发打印案例:
func main() {
	// 切记:无缓冲通道属于同步通道,双方goroutine必须都准备好才能进行
	ch := make(chan int)
	go printer(ch)
	for i := 1; i < 10; i++ {
		ch <- i
	}
	ch <- 0  // 没数据拉
	<-ch  // 搞定喊我
}
func printer(ch chan int) {
	for {
		data := <- ch
		if data == 0 {
			break
		}
		fmt.Println(data)
	}
	ch <- 0  // 我搞定啦
}

本案例的设计模式就是典型的生产者和消费者。

go语言单向通道

  1. go语言的类型系统提供了单方向的channel类型,顾名思义,单向 channel 就是只能用于写入或者只能用于读取数据。当然channel本身必然是同时支持读写的,否则根本没法用。
    假如一个 channel 真的只能读取数据,那么它肯定只会是空的,因为你没机会往里面写数据。同理,如果一个 channel 只允许写入数据,即使写进去了,也没有丝毫意义,因为没有办法读取到里面的数据。所谓的单向channel,其实只是对channel的一种使用限制。
  2. 单向通道的声明格式
    我们再将一个channel变量传递到一个函数时,可以通过将其指定为单向channel变量,从而限制该函数中对此channel的操作,比如只能往这个channel中写入数据,或者只能从这个channel中读取数据。
    单向 channel 变量的声明非常简单,只能写入数据的通道类型为chan<-,只能读取数据的通道类型为<-chan,格式如下:
var 通道实例 chan<- 元素类型    // 只能写入数据的通道
var 通道实例 <-chan 元素类型    // 只能读取数据的通道
  • 元素类型:通道包含的元素类型
  • 通道实例:声明的通道便令
  1. 单向通道的例子
func main() {
	ch := make(chan int)
	var writeChan chan<- int = ch  // 声明只写通道
	var readChan <-chan int = ch  // 声明只读通道
	go func() {
		writeChan <- 18
	}()
	fmt.Println(<-readChan)
}

当然使用make创建时,也可以创建只写入或者只读取的通道,

func main() {
	ch := make(<-chan int)
	var readChan <-chan int = ch
	<-readChan
}

但是一个不能写入数据的只读通道是毫无意义的。

time包中的单向通道

  1. time包中的计时器会返回一个Timer实例:
timer := time.NewTimer(time.Second)

time.Timer类型定义如下:

type Timer struct {
	C <-chan Time
	r runtimeTimer
}

C 通道的类型就是一种只能读取的单向通道。如果此处不进行通道方向约束,一旦外部向通道写入数据,将会造成其他使用到计时器的地方逻辑产生混乱。
因此,单向通道有利于代码接口的严谨性。
2. 关闭channel
关闭channel非常简单,直接使用go语言内置的close函数即可。
在介绍了如何关闭 channel 之后,我们就多了一个问题:如何判断一个 channel 是否已经被关闭?我们可以在读取的时候使用多重返回值的方式:
x, ok := <-ch
这个用法与 map 中的按键获取 value 的过程比较类似,只需要看第二个 bool 返回值即可,如果返回值是 false 则表示 ch 已经被关闭。
案例:

func main() {
	ch := make(chan int)
	go func() {
		ch <- 1
	}()
	//close(ch)
	// ok返回false代表channel已经关闭,返回true代表channel未关闭
	data, ok := <-ch
	fmt.Println(data, ok)
}

go语言无缓冲的通道

Go语言中无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。
如果两个 goroutine 没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。
阻塞指的是由于某种原因数据没有到达,当前协程(线程)持续处于等待状态,直到条件满足才解除阻塞。
同步指的是在两个或多个协程(线程)之间,保持数据内容一致性的机制。

在第 1 步,两个 goroutine 都到达通道,但哪个都没有开始执行发送或者接收。
在第 2 步,左侧的 goroutine 将它的手伸进了通道,这模拟了向通道发送数据的行为。这时,这个 goroutine 会在通道中被锁住,直到交换完成。
在第 3 步,右侧的 goroutine 将它的手放入通道,这模拟了从通道里接收数据。这个 goroutine 一样也会在通道中被锁住,直到交换完成。
在第 4 步和第 5 步,进行交换,并最终在第 6 步,两个 goroutine 都将它们的手从通道里拿出来,这模拟了被锁住的 goroutine 得到释放。两个 goroutine 现在都可以去做别的事情了。

为了讲得更清楚,让我们来看两个完整的例子。这两个例子都会使用无缓冲的通道在两个 goroutine 之间同步交换数据。
案例一:

var wg sync.WaitGroup

func init() {
	rand.Seed(time.Now().UnixNano())
}
func main() {
	// 使用无缓冲通道在两个goroutine之间同步交换数据
	ch := make(chan int)
	wg.Add(2)
	go player("彤彤", ch)
	go player("龙龙", ch)
	ch <- 1
	wg.Wait()
}
func player(name string, court chan int) {
	defer wg.Done()
	for {
		ball, ok := <-court
		if !ok {
			fmt.Printf("Player %s Won\n", name)
			return
		}
		n := rand.Intn(100)
		if n%13 == 0 {
			fmt.Printf("Player %s Missed\n", name)
			close(court)
			return
		}
		fmt.Printf("Player %s hit %d\n", name, ball)
		ball++
		court <- ball
	}
}

【示例 2】用不同的模式,使用无缓冲的通道,在 goroutine 之间同步数据,来模拟接力比赛。在接力比赛里,4 个跑步者围绕赛道轮流跑。第二个、第三个和第四个跑步者要接到前一位跑步者的接力棒后才能起跑。比赛中最重要的部分是要传递接力棒,要求同步传递。在同步接力棒的时候,参与接力的两个跑步者必须在同一时刻准备好交接。代码如下所示。

var wg sync.WaitGroup

func main() {
	baton := make(chan int)
	wg.Add(1)
	go Runner(baton)
	baton <- 1
	wg.Wait()
}
func Runner(baton chan int) {
	// 通过无缓冲通道实现多个goroutine之间同步交换数据
	var newRunner int
	// 等待接力棒
	runner := <-baton
	// 开始拿着接力棒跑步
	fmt.Printf("Runnber %d running with baton\n", runner)
	// 创建下一位跑步者
	if runner != 4 {
		newRunner = runner + 1
		fmt.Printf("Runner %d to the line\n", newRunner)
		go Runner(baton)
	}
	// 围绕跑道跑
	time.Sleep(time.Millisecond * 100)
	// 比赛结束了吗
	if runner == 4 {
		fmt.Printf("Runner %d finished, race over\n", runner)
		wg.Done()
		return
	}
	// 将接力棒给下一位跑步者
	fmt.Printf("Runner %d exchanged with runner %d\n", runner, newRunner)
	baton <- newRunner
}

在这两个例子里面我们使用无缓冲通道同步goroutine。

go语言带缓冲的通道

  1. Go语言中有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
  2. 这导致有缓冲通道和无缓冲通道之间有一个很大的不同:无缓冲通道保证进行发送和接收的goroutine会在同一时刻进行数据的交换,有缓冲通道没有这种保证。
  3. 在无缓冲通道的基础上,为通道增加一个有限大小的存储空间形成带缓冲通道。带缓冲通道在发送时无需等待接收方接收即可完成发送过程,并且不会发生阻塞,只有当存储空间满时才会发生阻塞。同理,如果缓冲通道中有数据,接收时将不会发生阻塞,直到通道中没有数据可读时,通道将会再度阻塞。
  4. 无缓冲通道保证收发过程同步。无缓冲收发过程类似于快递员给你电话让你下楼取快递,整个递交快递的过程是同步发生的,你和快递员不见不散。但这样做快递员就必须等待所有人下楼完成操作后才能完成所有投递工作。如果快递员将快递放入快递柜中,并通知用户来取,快递员和用户就成了异步收发过程,效率可以有明显的提升。带缓冲的通道就是这样的一个“快递柜”。
  5. 阻塞条件
    带缓冲通道在很多特性上和无缓冲通道是类似的。无缓冲通道可以看作是长度永远为 0 的带缓冲通道。因此根据这个特性,带缓冲通道在下面列举的情况下依然会发生阻塞:
  • 带缓冲通道被填满时,尝试再次发送数据时发生阻塞
  • 带缓冲通道为空时,尝试再次接收数据时发生阻塞
  1. 为什么go语言对通道要限制长度,而不提供无限制长度的通道
    我们知道通道(channel)是两个goroutine间通信的桥梁,使用到goroutine的代码,必然有一方提供数据,一方负责消费数据。
    当提供数据一方的数据供给速度大于消费方的数据处理速度时,如果不限制通道的长度,那么内存将不断膨胀直到应用崩溃,因此限制通道的长度有利于约束数据提供方的供给速度,供给数量必须在消费方处理量+通道长度的范围内才能正常的处理数据。

标签:02channel,缓冲,goroutine,阻塞,并发,go,接收,channel,通道
From: https://www.cnblogs.com/mayanan/p/16592131.html

相关文章