goruntine和channel

前面几个回合,我们都是实例,但在go中有个很常用的东东,我们代码使用的不多,这一次我们单独拿来讲讲。

goroutine

goroutine是go语言特有的并发体,是一种轻量级的线程,由go关键字启动。

goroutine采用的是半抢占市的协作调度,只有当前goroutine发生阻塞时才会导致调度(也就是goroutine的切换)

那么项目中使用了多个goroutine,如何在不同的goroutine之间通信呢?

学习go的过程中,想必你应该知道有这么一个经典的句子:

Do not communicate by sharing memory; instead, share memory by communicating.

goroutine的通信用通道

img

通道是个啥?

一个通道相当于一个先进先出(FIFO)的队列,通道中的各个元素是严格地按照发送的顺序排列的,先被发送通道的一定会先被接收。元素的发送和接收都需要用到操作符<-

这里有一个FIFO简单的例子

package main

import "fmt"

func main() {
	ch := make(chan int, 5)
	ch <- 1
	ch <- 2
	ch <- 3
	ch <- 4
	ch <- 5

	fmt.Println("1-", <-ch)
	fmt.Println("2-", <-ch)
	ch <- 6
	fmt.Println("3-", <-ch)
	fmt.Println("4-", <-ch)
	fmt.Println("5-", <-ch)
	fmt.Println("6-", <-ch)
	close(ch)
}

/*
打印的内容如下:
1- 1
2- 2
3- 3
4- 4
5- 5
6- 6
*/

我们再来看一下通道的常规操作:

创建通道

通道(channel)分两种(容量是否为0):

  • 缓冲通道
  • 非缓冲通道
// 缓冲通道
ch1 := make(chan int, 10)
ch2 := make(chan bool, 2)


// 非缓冲通道
ch3 := make(chan int)
ch4 := make(chan bool, 0)

发送通道数据

// 创建一个空接口通道,注意定义的通道类型有
ch := make(chan interface{})
// 将0放入通道中
ch <- 0
// 将hello字符串放入通道中
ch <- "hello"

接收通道数据

1. 阻塞接收数据

阻塞模式接收数据时,将接收变量作为<-操作符的左值,格式如下:

data := <-ch

执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。

2. 非阻塞接收数据

使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:

data, ok := <-ch

// data:表示接收到的数据。未接收到数据时,data 为通道类型的零值
// ok:表示是否接收到数据。

非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel 进行,可以参见后面的内容。

3. 接收任意数据,忽略接收的数据

阻塞接收数据后,忽略从通道返回的数据,格式如下:

<-ch

4. 循环接收

通道的数据接收可以借用 for range 语句进行多个元素的接收操作,格式如下:

package main

import (
    "fmt"

    "time"
)

func main() {

    // 构建一个通道
    ch := make(chan int)

    // 开启一个并发匿名函数
    go func() {

        // 从3循环到0
        for i := 3; i >= 0; i-- {

            // 发送3到0之间的数值
            ch <- i

            // 每次发送完时等待
            time.Sleep(time.Second)
        }

    }()

    // 遍历接收通道数据
    for data := range ch {

        // 打印通道数据
        fmt.Println(data)

        // 当遇到数据0时, 退出接收循环
        if data == 0 {
                break
        }
    }

}

关闭通道

ch := make(chan string)

...

close(ch)

通道特性

  • 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的(并发安全)
  • 发送操作和接收操作中对元素值的处理都是不可分割的。
  • 发送操作在完全完成之前会被阻塞,接收操作也是一样。
  • 对于缓冲通道:如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走;如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。
  • 对于非缓冲通道:无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。

注意点:

  • 关闭通道要在发送方关闭,关闭后如果channel内还有元素,并不会对接下来的接收产生影响
  • 单向通道最主要的用途就是约束其他代码的行为
  • 通过函数的参数类型或者返回值类型来限制(Go的语法糖)。
func(ch chan<- int);传入双向通道,在函数里面调用ch只能发送
func() (ch <-chan int);返回双向通道,在函数外面里面调用ch只能接收

说到这里,我们提一下channel独有的关键字——select。

A “select” statement chooses which of a set of possible send or receive operations will proceed. It looks similar to a “switch” statement but with the cases all referring to communication operations.

一个select语句用来选择哪个case中的发送或接收操作可以被立即执行。它类似于switch语句,但是它的case涉及到channel有关的I/O操作。

说完这些概念性的玩意儿,我们还是来几个实例,感受一下goroutine配合channel使用的快感。


chan配合select实现超时处理

func main() {
	timeout := make(chan bool)
	go func() {
		time.Sleep(3e9)
		timeout <- true
	}()
	ch := make(chan int)
	select {
	case <-ch:
	case <-timeout:
		fmt.Println("timeout!")
	}
}

非缓冲通道,监听信号量

// 来自gin文档的例子
func main() {
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		time.Sleep(5 * time.Second)
		c.String(http.StatusOK, "Welcome Gin Server")
	})

	srv := &http.Server{
		Addr:    ":8080",
		Handler: router,
	}

	go func() {
		// 服务连接
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen: %s\n", err)
		}
	}()

	// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
	quit := make(chan os.Signal)
	signal.Notify(quit, os.Interrupt)
	<-quit
	log.Println("Shutdown Server ...")

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal("Server Shutdown:", err)
	}
	log.Println("Server exiting")
}

当然还有其他应用场景,如消息传递、消息过滤,事件订阅与广播,请求、响应转发,并发控制,同步与异步等,可参考下面的文章:

总结了才知道,原来channel有这么多用法!


实例代码点击见githubgithub.com/puresai/go-example/tree/main/demo8-goroutine-channel

参考:


goruntine和channel
https://blog.puresai.com/2021/02/12/goexample8/
作者
puresai
许可协议