goroutine

Go语言的特色之一就是支持协程。协程是一种轻量级的线程,其在操作系统中通常被称为用户态线程,因为它们是由用户程序自己实现的,而不是由操作系统内核实现的。与传统的线程相比,协程具有以下优点:占用资源少、切换成本低、并发操作高效。

在程序启动时,Go 运行时系统会创建一个主协程,该协程负责程序的初始化和启动。在程序运行过程中,通过 go 关键字可以创建新的协程。主协程和新协程可以并行执行,互不影响。

1
2
3
4
5
6
func main(){
go func(){
//协程代码
}()
//主协程代码
}

主协程

封装 main 函数的goroutine称为主goroutine

主goroutine会进行一系列的初始化工作:

  1. 创建一个特殊的 defer 语句,用于在主goroutine退出时做必要的善后处理。因为主goroutine也可能非正常的结束
  2. 启动专用于在后台清扫内存垃圾的goroutine,并设置GC可用的标识
  3. 执行 mian 包中的 init 函数
  4. 执行 mian 函数

CSP(communicating sequential processes)并发模型

不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”

Go的CSP并发模型,是通过 goroutinechannel 来实现的

channel 是Go语言中各个并发结构体(goroutine)之间的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。传数据用 channel <- data ,取数据用 <-channel

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {

messages := make(chan string)
//创建了一个通道(channel)变量,通道的类型是`string`
go func() { messages <- "ping" }()

msg := <-messages
fmt.Println(msg)
}

Context

  • Context只有两个功能,跨API或者进程间:
    • 携带键值对
    • 传递取消信号(主动取消、时限/超时自动取消)
  • Context创建空上下文方式:

    • context.Background() - context的默认值,所有的context都应该从它衍生出来
    • context.TODO() - 不确定要使用哪个上下文时,可以将其用作占位符
  • Context 有四个 with 系列函数进行派生:

    1
    2
    3
    4
    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
    func WithValue(parent Context, key, val interface{}) Context

这四个函数都基于 父Context 衍生,通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,如下图:

WithValue()

WithValue() 函数可以让Context实现携带键值对的功能。该函数传入的 Context 是 parent Context 。返回的 Context 与其是派生关系。(相当于上面 Context tree 的子节点)

例子:

1
2
3
4
5
6
7
8
9
10
func main() {
a := context.Background() // 创建上下文
b := context.WithValue(a, "k1", "v1") // 塞入一个kv
c := context.WithValue(b, "k2", "v2") // 塞入另外一个kv
d := context.WithValue(c, "k1", "vo1") // 覆盖一个kv

fmt.Printf("k1 of b: %s\n", b.Value("k1"))
fmt.Printf("k1 of d: %s\n", d.Value("k1"))
fmt.Printf("k2 of d: %s\n", d.Value("k2"))
}

输出如下:

1
2
3
4
k1 of b: v1
k1 of d: vo1
k2 of d: v2
//在获取键值对时,会先从当前context中查找,没有找到会在从父context中查找该键对应的值直到在某个父context中返回 nil 或者查找到对应的值

超时控制

withTimeout()withDeadline() 来做超时控制,它俩的作用是一样的,就是传递的时间参数不同。它们都通过传入的时间来自动取消Context,要注意的是它们都会返回一个 cancelFunc 方法,通过调用这个方法可以达到提前进行取消,不过在使用的过程还是建议在自动取消后也调用 cancelFunc 去停止定时减少不必要的资源浪费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main()  {
HttpHandler1()
}

func NewContextWithTimeout1() (context.Context,context.CancelFunc) {
return context.WithTimeout(context.Background(), 3 * time.Second)
}

func HttpHandler1() {
ctx, cancel := NewContextWithTimeout1()
defer cancel() //超时自动取消
deal1(ctx, cancel)
}

func deal1(ctx context.Context, cancel context.CancelFunc) {
for i:=0; i< 10; i++ {
time.Sleep(1*time.Second)
select {
case <- ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Printf("deal time is %d\n", i)
cancel() //手动控制取消
}
}
}

输出结果如下:

1
2
deal time is 0
context canceled

WithCancel()

日常业务开发中往往为了完成一个复杂的需求会开多个gouroutine去做一些事情,这就导致一次请求中开了多个goroutine却无法控制他们,这时我们就可以使用 withCancel 来衍生一个context传递到不同的goroutine中,当我想让这些goroutine停止运行,就可以调用 cancel 来进行取消。

Done()

Done() 函数确认上下文是否完成,无论上下文是因为什么原因结束的,都可以通过调用其 Done() 方法确认:该方法会返回一个通道(chan),该通道会在上下文完成时被关闭,任何监听该通道的函数都会感应到对应上下文完成的事件。

  • 没有关闭的时候, case <- ctx.Done() 会阻塞住
  • 关闭之后,每次 <- ctx.Done() 都会返回一个零值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main()  {
ctx,cancel := context.WithCancel(context.Background())
go Speak(ctx) //启动一个讲话程序,每隔1s说一话
time.Sleep(10*time.Second)
cancel() //main函数在10s后执行cancel,speak检测到取消信号就会退出
time.Sleep(1*time.Second)
}

func Speak(ctx context.Context) {
for range time.Tick(time.Second){
select {
case <- ctx.Done(): //监听上下文的取消信号
fmt.Println("我要闭嘴了")
return
default:
fmt.Println("balabalabalabala")
}
}
}

输出结果如下:

1
2
3
4
5
balabalabalabala
balabalabalabala
....
balabalabalabala
我要闭嘴了

`Err()``

Err() 函数返回上下文结束的原因,如果上下文没有结束,返回 nil ,如果上下文已经结束,获取上下文错误