PHP2GO-第五课(协程间的通信)

126 阅读6分钟

你好,我是小小酥,在上一篇文章中,我们了解了并行与并发的区别,进程、线程与协程的区别,也创建了我们自己的协程,今天我们将继续了解协程之间的通信方式,从而让你能更好的使用协程,准备发车~

CSP并发模型

不要通过共享内存来通信,通过通信来共享内存“

了解过go的朋友一定听说过这句经典名言。

这其实是一种(CSP)并发模型,和常见的通过共享内存来通信的方式相比,CSP倡导通过通信来实现线程/协程之间的交互,避免了共享内存中多个数据同时访问一块内存需要考虑的数据冲突,如何合理加锁等复杂问题。这种设计一定程度上会增加代码的复杂度,但是逻辑简单清楚,可以保证系统有高正确性,不会出现竞争的出现。

而在go语言中,则是在语言层面提供了channel这一数据结构,使得我们可以很好的通过CSP这一范式进行写成之间的通信。

Channel

初始化

package main

func main() {
  var ch chan int // 定义一个channel

  ch = make(chan int) // 初始化一个无缓冲channel

  ch2 := make(chan int,5) // 定义并初始化一个缓冲区为5的channel
}

channel和切片,结构体,map等一样也是一种复合型的数据结构,定义的结构为chan T(T为数据类型)。

我们同样可以通过var关键字来定义一个channel,也可以通过make(chan T)的方式来初始化一个channel。

通过上面的代码,我们还可以看到,channel分为了无缓冲区channel和有缓冲区channel,对于有缓冲区的channel,我们通过make的第二个参数即可指定缓冲区的长度,因此在不同的业务场景下我们可以根据需要选择对应的channel类型。

通过channel通信

首先我们了解一下,在go语言中,我们通过 即可向channel发送一条数据,或者读取channel中的数据。其中 ch←10 既代表我们要向channel发送一条为10的数据,←ch 则代表我们要从channel中读取一条数据。是不是很简单?

那么接着,我们来理解下下面这段代码,我们在main中先创建了一个channel,并且启动了一个background的协程,接着我们向channel发送了一条值为10的数据,并在启动的background协程中等待从channel中读取数据。

  1. 我们在main goroutine中定义了一个无缓冲区,类型为int的channel
  2. 启动了一个名为background的gorotuine,并将channel作为参数进行了传递
  3. 启动后的background goroutine 向channel发送数据
  4. main goroutine等待从channel中读取数据并赋值给data,此时background goroutine阻塞
  5. background gorotuine中channel接收到数据后解除阻塞,打印data
package main

import (
    "fmt"
)

func main() {
  ch := make(chan int) // 1
  go background(ch) // 2
	data := <- ch // 3
  fmt.Println(data) // 5
}

func background(ch chan int) {
		ch <- 10 // 4
}

无缓冲区channel和有缓冲区channel

最后我们再来看看无缓冲区的channel和有缓冲区channel的区别。

顾名思义,有缓冲区的channel就是比无缓冲区的channel多了一个缓冲区。

发送消息

对于一个无缓冲区的channel,当一个goroutine向channel发送消息后,若没有其他gorotuine从当前channel读取消息,那么这个goroutine会阻塞,直到消息被读走才会取消阻塞。

对于一个有缓冲区的channel,当一个goroutine向channel发送消息后,只要此时缓冲区没满,就会将数据存在缓冲区,即使没有其他gorotuine从当前channel读取消息,gorotuine也不会阻塞,直到缓冲区的数据满了以后,gorotuine才会阻塞。

package main

func goroutine1() {
	ch := make(chan int)
	ch <- 1 // 此时因为没有gorotuine从ch读取数据,所以goroutine阻塞
}

func goroutine2() {
	ch := make(chan int,3)
	ch <- 1 // ch的缓冲区为3,此时goroutine不会阻塞
	ch <- 2
	ch <- 3
	ch <- 4 // 此时缓冲区已满,gorotuine阻塞
}

接收消息

同理,对于一个无缓冲区的channel,当一个goroutine从channel里读取数据时,若没有其他goroutine向当前channel发送数据,那么这个gorotuine阻塞,直到有其他goroutine向这个channel发送数据。

对于一个有缓冲区的channel,当一个goroutine从channel读取消息时,如果缓冲区有数据,则能读取到数据不会阻塞,直到缓冲区没有数据时,当前gorotuine才会阻塞。

餐后甜点

你还记得上节课的思考题么?既下面这段代码执行后会发生什么?

package main

import "fmt"

func main() {
	go func() {
		fmt.Println("hello")
	}()

	go func() {
		fmt.Println("world")
	}()
}

尝试过后,你会发现我们期望的”hello”,”world”并没有如预期输出。这是为什么呢?

其实是因为在go语言中,main函数其实是一个特殊的gorotuine,我们称其为main goroutine。main gorotuine执行结束后,进程内其他的gorotuine也会被强制结束掉。

在这段代码中,main goroutine启动了两个独立的协程去打印字符串。但是由于main gorotuine做的事情很简单,创建完两个协程后就结束了,所以打印字符串的命令还没来得及执行。

我们可以怎么解决这个问题呢?

package main

import (
	"fmt"
	"time"
)

func main() {
	go func() {
		fmt.Println("hello")
	}()

	go func() {
		fmt.Println("world")
	}()

	time.Sleep(1 * time.Second)
}

和第一版代码相比,我们在main函数中创建完两个协程后,通过time.Sleep 函数让程序睡眠了1秒,此时”hello“,”world”被成功打印。因为在main gorotuine睡眠的一秒钟内,足以让两个协程完成运行了。可是这种操作很显然是不够优雅和可控的。因为在程序中我们并不能很好的判断出其他的协程需要多久才能执行完成。所以这时我们今天学到的channel就能派上用场了。

我们会在下一篇文章中展示如何通过channel来解决这一问题,再此之前,期望你能先尝试实现一下~

下期预告

今天我们了解了csp并发模型,并且学习了go语言中一个很重要的数据结构channel,通过channel,我们即可完成多协程间的通信。

下一次我们会继续了解go语言中一些场景的并发控制手段,从而适配各种引用场景敬请期待

如果你喜欢我的文章,欢迎关注我的公众号,万分感谢!

qrcode_for_gh_83255ce34399_258.jpg