go channel && Context

  在Go语言中,每一个并发的执行单元叫作一个goroutine;

当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。

f()    // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait

是不是有点像脚本语言里面的&作用

package main

import "fmt"
import "time"


func spinner(delay time.Duration) {
    for {
        for _, r := range `-\|/` {
            fmt.Printf("\r%c", r)
            time.Sleep(delay)
        }
    }
}

func fib(x int) int {
    if x < 2 {
        return x
    }
    return fib(x-1) + fib(x-2)
}

func main() {
    go spinner(1 * time.Millisecond)
    const n = 30
    fibN := fib(n) // slow
    fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}

结果为:
-
\
|
/
-
\
Fibonacci(30) = 832040
View Code

Channels

  一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan in

  使用内置的make函数,我们可以创建一个channel:

ch := make(chan int) // ch has type 'chan int'

  和map类似,channel也对应一个make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。

两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。

  一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使用<-运算符。在发送语句中,<-运算符分割channel和要发送的值。在接收语句中,<-运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。

ch <- x  // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch     // a receive statement; result is discarded

  Channel支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。

对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。

不带缓存的Channels

  一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。

  基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时,接收者收到数据发生在再次唤醒唤醒发送者goroutine之前;在讨论并发编程时,当我们说x事件在y事件之前发生(happens before),我们并不是说x事件在时间上比y时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了。

ch = make(chan int)    // unbuffered channel
ch = make(chan int, 0) // unbuffered channel

 

 

串联的Channels

Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)

func main() {
    naturals := make(chan int)
    squares := make(chan int)

    // Counter
    go func() {
        for x := 0; ; x++ {
            naturals <- x
        }
    }()

    // Squarer
    go func() {
        for {
            x := <-naturals
            squares <- x * x
        }
    }()

    // Printer (in main goroutine)
    for {
        fmt.Println(<-squares)
    }
}

  上述是一个永久发送逻辑;但是如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现:

当一个channel被关闭后,再向该channel发送数据将导致panic异常。当一个被关闭的channel中已经发送的数据都被成功接收后,后续的接收操作将不再阻塞,它们会立即返回一个零值;但是关闭上面例子中的naturals变量对应的channel并不能终止循环,它依然会收到一个永无休止的零值序列

  有没有办法直接测试一个channel是否被关闭,但是接收操作有一个变体形式:它多接收一个结果,多接收的第二个结果是一个布尔值ok,ture表示成功从channels接收到值,false表示channels已经被关闭并且里面没有值可接收。使用这个特性,我们可以修改squarer函数中的循环代码,当naturals对应的channel被关闭并没有值可接收时跳出循环,并且也关闭squares对应的channel.

// Squarer
go func() {
    for {
        x, ok := <-naturals
        if !ok {
            break // channel was closed and drained
        }
        squares <- x * x
    }
    close(squares)
}()

    Go语言的range循环可直接在channels上面迭代。使用range循环是上面处理模式的简洁语法,它依次从channel接收数据,当channel被关闭并且没有值可接收时跳出循环。

func main() {
    naturals := make(chan int)
    squares := make(chan int)

    // Counter
    go func() {
        for x := 0; x < 100; x++ {
            naturals <- x
        }
        close(naturals)
    }()

    // Squarer
    go func() {
        for x := range naturals {
            squares <- x * x
        }
        close(squares)
    }()

    // Printer (in main goroutine)
    for x := range squares {
        fmt.Println(x)
    }
}

 

带缓存的Channels

带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的

ch = make(chan string, 3)

 

   向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素

ch <- "A"
ch <- "B"
ch <- "C"

 

 如果有第四个发送操作将发生阻塞;

  Channel和goroutine的调度器机制是紧密相连的,如果没有其他goroutine从channel接收,发送者——或许是整个程序——将会面临永远阻塞的风险。如果你只是需要一个简单的队列,使用slice就可以了。

var ch1 chan int       // ch1是一个正常的channel,是双向的
var ch2 chan<- float64 // ch2是单向channel,只用于写float64数据
var ch3 <-chan int     // ch3是单向channel,只用于读int数据
chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
<-chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。
可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel:


chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
<-chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。
可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel:

c := make(chan int, 3)
    var send chan<- int = c // send-only
    var recv <-chan int = c // receive-only
    send <- 1
    //<-send //invalid operation: <-send (receive from send-only type chan<- int)
    <-recv
    //recv <- 2 //invalid operation: recv <- 2 (send to receive-only type <-chan int)

    //不能将单向 channel 转换为普通 channel
    d1 := (chan int)(send) //cannot convert send (type chan<- int) to type chan int
    d2 := (chan int)(recv) //cannot convert recv (type <-chan int) to type chan int

Go channel 

channel 是 Go 提供的语言级协程通讯方式,是 Go 推荐的多协程协调通讯方式。

  • 可以理解成阻塞消息队列
  • channel 是并发安全的,支持多生产多消费

(1)通道初始化方式 

  • ch := make(chan 消息类型) 无缓冲通道
  • ch := make(chan 消息类型, 缓冲大小) 有缓冲通道

(2)通道的读写 

    • data := <-ch 阻塞读出(等待直到通道有数据)
    • data, ok := <-ch 阻塞读出 ok 表示通道是否关闭
      • 通道未关闭时 ok = true data != 零值
      • 通道关闭时 ok = false data = 零值
    • 多路复用 select
    • 利用 select + time.Timer 实现读超时
    • 利用 select + default 实现非阻塞读
    • 语法糖:循环读 for data := range ch 通道关闭退出循环
    • 读取并丢弃 <- ch
    • ch <- data 阻塞写入(直到通道有空间)
    • 多路复用 select
    • 利用 select + time.Timer 实现写超时
    • 利用 select + default 实现非阻塞写

注意:读写 nil channel 将永远阻塞

(3)单向通道与双向通道 

  • var readOnlyChannel <-chan 消息类型 = ch 只读通道,向只写通道读将抛出编译错误
  • var writeOnlyChannel chan<- 消息类型 = ch 只写通道,向只读通道写将抛出编译错误

(4)关闭通道 

  • close(ch) 只能执行一次,再次关闭将触发 panic
  • 获取是否关闭 data, ok := <-ch 阻塞读出 ok 表示通道是否关闭
    • 通道未关闭时 ok = true data != 零值
    • 通道关闭时 ok = false data = 零值
  • 写入关闭的 channel 将触发 panic
  • 读取关闭的 channel 立即返回零值
  • for range ch 将推出循环

(5)无缓冲通道和缓冲通道 

  • 无缓冲通道
    • 创建方式 ch := make(chan 消息类型) 或 ch := make(chan 消息类型, 0)
    • 特点,对于一个未关闭的通道
      • 某协程向该通道发送一个消息将阻塞到,另一个协程取出该消息
      • 某协程从该通道读取一个消息将阻塞到,另一个协程发送一个消息
    • cap 和 len 函数返回均为 0
  • 缓冲通道

    • 创建方式 ch := make(chan 消息类型, 缓冲大小) 且 缓冲大小 > 0
    • 特点,对于一个未关闭的通道
      • 某协程向该通道发送一个消息
        • 若该通道缓冲区已满,将阻塞到缓冲区有空间为止
        • 否则,直接返回
      • 某协程从该通道接收一个消息
        • 若该通道缓冲区是空的,将阻塞到缓冲区有数据为止
        • 否则,返回数据
    • cap 和 len 函数可以获取 缓冲通道的容量和数据长度

      	a3 := make(chan string, 2)
      	a3 <- ""
      	fmt.Println(cap(a3), len(a3))

(6)实现超时和非阻塞 

  • 通过 select 和 time.Timer 实现
  • 通过 select 和 default 实现

(7)通道多路复用 select 

go channel 类似于阻塞 IO,也存在阻塞,操作系统提供了 IO 多路复用功能,类似的 Go 语言为 channel 提供了多路复用功能

(多路复用:将多个 阻塞 聚合在一个阻塞点)

基本语法

select {
case 读写channel:
    操作
case 读写channel:
    操作
default: // 可选
    操作
}

select 只会执行一次,如果需要多次,一般需要使用 for 循环包裹

for {
    select {
    // ...
    }
}

如果 select 包含 default 分支,则实现非阻塞效果,即,当其他 case 分支没有数据时,执行 default 分支

select {
case ad := <- a:
    fmt.Println(ad)
default:
    fmt.Println("default")
}

如果 select中同时有多个 channel 就绪,则只会处理最上面就绪的那一个(可以理解成 每个 case 都加了 break

func selectMultiChannel(a <-chan string, b <-chan string){
	for i := 0; i< 10; i++ {
		select {
		case ad := <- a:
			fmt.Println(i, ad)
		case bd := <- b:
			fmt.Println(i, bd)
		}
	}
}
func Chanvar() {
	a := make(chan string, 1)
	b := make(chan string, 1)
	b <- "b"
	a <- "a"
    go selectMultiChannel(a, b)
	// 输出
	// 0 a
	// 1 b
	time.Sleep(1000 * time.Microsecond)
}

利用 time.Timer 可以实现等待超时效果

func channelTimeout(a <-chan string) {
	select {
	case ad := <- a:
		fmt.Println(ad)
	case <- time.After(500 * time.Millisecond):
		fmt.Println("Timeout")
	}
}
func Chanvar() {
	a2 := make(chan string, 1)
	go channelTimeout(a2)
	time.Sleep(600 * time.Millisecond)
	// 输出 Timeout
	a2 <- "message a"
	time.Sleep(100 * time.Millisecond)
}

select 还允许 多路复用 写入消息

func selectWriteChannel(a chan<- string) {
	select {
	case a <- "从 select中写入 a":
		fmt.Println("写入 a 成功")
	default:
		fmt.Println("Default 写入 a 失败")
	}
}

	a3 := make(chan string, 2)
	go selectWriteChannel(a3)
	time.Sleep(100 *time.Millisecond)
	fmt.Println(cap(a3), len(a3))
	go selectWriteChannel(a3)
	time.Sleep(100 *time.Millisecond)
	fmt.Println(cap(a3), len(a3))
	go selectWriteChannel(a3)
	time.Sleep(100 *time.Millisecond)
	fmt.Println(cap(a3), len(a3))
	// 输出
	// 写入 a 成功
	// 2 1
	// 写入 a 成功
	// 2 2
	// Default 写入 a 失败
	// 2 2

(8)实现原理 

Go 语言设计与实现 - 6.4 Channel

  • 对于有缓冲的 Channel,数据队列是通过数组实现的循环队列,内存预分配 (mallocgc 分配内存)

3、标准库提供并发工具 

主要位于 sync 和 atomic 包

4、Context 控制协程 

博客1 Go 语言设计与实现 - 6.1 上下文 Context

context.Context 是 Go 中用来控制协程树的接口,具有如下功能

  • 使用 withXxx(parent) 系列函数可以构造一颗 Context 树
  • 在对某个 Context 调用 cancel 函数,则可以结束当前 Context 及子孙 Context
  • 可以在创建 Context 时,为该 Context 绑定一对 KV (少用),所有子孙 Context 可以通过 Key 查询自己及祖宗节点的 Value
  • 标准库提供,存在多种单一功能的 Context,通过 withXxx(parent) 创建
    • 具有超时或者定时取消功能的 Context
    • 绑定一对 KV

context.Context 相关API

  • 根 Context
    • context.Background() 是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来
    • context.TODO() 应该只在不确定应该使用哪种上下文时使用
  • 传递一个 父 Context 创建一个新的 Context
    • func WithCancel(parent Context) (ctx Context, cancel CancelFunc) 创建一个可取消的 Context 调用 cancel 返回值即可取消子孙 Context
    • func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) 创建一个可取消的 Context,调用 cancel 返回值即可取消子孙 Context,同时在 deadline 时刻自动取消
    • func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 创建一个可取消的 Context,调用 cancel 返回值即可取消子孙 Context,同时在 timeout 后自动取消
    • func WithValue(parent Context, key, val interface{}) Context 创建一个 Context 附加一个值
  • context.Context 接口函数语义
    • Deadline() (deadline time.Time, ok bool) 获取设置的截止时间(如果当前 Context 和 祖宗 Context 设置了)。
      • 如果设置了截止时间,第一个返回值是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值 ok == true
      • 如果没有设置截止时间,返回零值(ok == false
    • Done() <-chan struct{} 获取一个只读 Channel,调用多次返回一样的值,如果当前 Context 或 祖宗 Context 取消了,则该 Channel 将被关闭,直接返回(结合 select 即可实现协程树控制)
    • Err() error 返回当前 Context 或 祖宗 Context 是否取消
      • 未取消:返回 nil
      • 到达Deadline而取消:返回 context deadline exceeded
      • 手动调用 Cancel:返回 context canceled
    • Value(key interface{}) interface{} 当前 Context 或 祖宗 Context 上绑定的值
posted @ 2021-07-04 10:45  codestacklinuxer  阅读(168)  评论(0编辑  收藏  举报