[Golang]-5 协程、通道及其缓冲、同步、方向和选择器

协程

Go 协程 在执行上来说是轻量级的线程。

代码演示

import (
	"fmt"
	"time"
)

func f(from string) {
	for i := 0; i < 3; i++ {
		fmt.Println(from, ":", i)
		time.Sleep(100)
	}
}

func main() {

	// 假设我们有一个函数叫做 f(s)。我们使用一般的方式调并同时运行。
	f("direct")

	// 使用 go f(s) 在一个 Go 协程中调用这个函数。这个新的 Go 协程将会并行的执行这个函数调用。
	go f("goroutine")

	// 可以为匿名函数启动一个 Go 协程。
	go func(msg string) {
		for i := 0; i < 3; i++ {
			fmt.Println(msg, "-", i)
			time.Sleep(100)
		}
	}("going")

	// 现在这两个 Go 协程在独立的 Go 协程中异步的运行,所以我们需要等它们执行结束。
	// 这里的 Scanln 代码需要我们在程序退出前按下任意键结束。
	var input string
	fmt.Scanln(&input)
	fmt.Println("done:" + input)
}

输出:

direct : 0
direct : 1
direct : 2
going - 0
goroutine : 0
goroutine : 1
going - 1
going - 2
goroutine : 2
done:my

当我们运行这个程序时,将首先看到阻塞式调用的输出,然后是两个 Go 协程的交替输出。
这种交替的情况表示 Go 运行时是以异步的方式运行协程的。

通道

通道 是连接多个 Go 协程的管道。你可以从一个 Go 协程将值发送到通道,然后在别的 Go 协程中接收。

func main() {
	// 使用 make(chan val-type) 创建一个新的通道。通道类型就是他们需要传递值的类型。
	messages := make(chan string)

	// 使用 channel <- 语法 发送 一个新的值到通道中。
	// 这里我们在一个新的 Go 协程中发送 "ping" 到上面创建的messages 通道中。
	go func() {
		messages <- "ping1"
		messages <- "ping2"
	}()

	// 使用 <-channel 语法从通道中 接收 一个值。
	// 这里将接收我们在上面发送的 "ping" 消息并打印出来。
	msg1 := <-messages
	fmt.Println(msg1)

	fmt.Println(<-messages)
	fmt.Println(<-messages)
}

输出

ping1
ping2
fatal error: all goroutines are asleep - deadlock!

我们运行程序时,通过通道,消息 "ping" 成功的从一个 Go 协程传到另一个中。

默认发送和接收操作是阻塞的,直到发送方和接收方都准备完毕。这个特性允许我们,不使用任何其它的同步操作,来在程序结尾等待消息 "ping"。

通道缓冲

默认通道是 无缓冲 的,这意味着只有在对应的接收(<- chan)通道准备好接收时,才允许进行发送(chan <-)。
可缓存通道允许在没有对应接收方的情况下,缓存限定数量的值。

func main() {
	// 这里我们 make 了一个通道,最多允许缓存 3 个值。
	messages := make(chan string, 3)

	// 因为这个通道是有缓冲区的,即使没有一个对应的并发接收方,我们仍然可以发送这些值。
	messages <- "ping1"
	messages <- "ping2"
	messages <- "ping3"

	fmt.Println(<-messages)
	fmt.Println(<-messages)
	fmt.Println(<-messages)
}

输出

ping1
ping2
ping3

通道同步

我们可以使用通道来同步 Go 协程间的执行状态。这里是一个使用阻塞的接受方式来等待一个 Go 协程的运行结束。

// 这是一个我们将要在 Go 协程中运行的函数。
// done 通道将被用于通知其他 Go 协程这个函数已经工作完毕。
func worker(done chan bool) {
	fmt.Print("working...")
	time.Sleep(time.Second)
	fmt.Println("done")

	// 发送一个值来通知我们已经完工啦。
	done <- true
}

func main() {
	// 运行一个 worker Go协程,并给予用于通知的通道。
	done := make(chan bool, 1)
	go worker(done)

	//程序将在接收到通道中 worker 发出的通知前一直阻塞。
	<-done
}

输出:

working...done

如果你把 <- done 这行代码从程序中移除,程序甚至会在 worker还没开始运行时就结束了。

通道方向

当使用通道作为函数的参数时,你可以指定这个通道是不是只用来发送或者接收值。这个特性提升了程序的类型安全性。

// ping 函数定义了一个只允许发送数据的通道。尝试使用这个通道来接收数据将会得到一个编译时错误。
func ping(pings chan<- string, msg string) {
	pings <- msg
}

// pong 函数允许通道(pings)来接收数据,另一通道(pongs)来发送数据。
func pong(pings <-chan string, pongs chan<- string) {
	msg := <-pings
	pongs <- msg
}

func main() {
	pings := make(chan string, 1)
	pongs := make(chan string, 1)
	ping(pings, "passed message")
	pong(pings, pongs)
	fmt.Println(<-pongs)
}

输出:

passed message

通道选择器

Go 的通道选择器 让你可以同时等待多个通道操作。
Go 协程和通道以及选择器的结合是 Go 的一个强大特性。

import (
	"fmt"
	"time"
)

func main() {
	// 在我们的例子中,我们将从两个通道中选择。
	c1 := make(chan string)
	c2 := make(chan string)

	// 各个通道将在若干时间后接收一个值,这个用来模拟例如并行的 Go 协程中阻塞的 RPC 操作
	go func() {
		time.Sleep(time.Second * 1)
		c1 <- "one"
	}()
	go func() {
		time.Sleep(time.Second * 2)
		c2 <- "two"
	}()

	// 我们使用 select 关键字来同时等待这两个值,并打印各自接收到的值。
	for i := 0; i < 2; i++ {
		select {
		case msg1 := <-c1:
			fmt.Println("received", msg1)
		case msg2 := <-c2:
			fmt.Println("received", msg2)
		}
	}
}

输出:

received one
received two

我们首先接收到值 "one",然后就是预料中的 "two"了。

注意从第一次和第二次 Sleeps 并发执行,总共仅运行了两秒左右。

posted @ 2020-12-16 17:35  哆啦梦乐园  阅读(236)  评论(0编辑  收藏  举报