Go36-32-context.Context

context.Context

sync.WaitGroup类型是一个实现一对多goroutine协作流程的同步工具。还有另一种工具也可以实现这种协作流程。

专注于为中小企业提供成都做网站、成都网站制作服务,电脑端+手机端+微信端的三站合一,更高效的管理,为中小企业昌邑免费做网站提供优质的服务。我们立足成都,凝聚了一批互联网行业人才,有力地推动了上千多家企业的稳健成长,帮助中小企业通过网站建设实现规模扩充和转变。

回顾sync.WaitGroup实现协作流程

在使用WaitGroup的时候,建议是用“先统一Add,再并发Done,最后Wait”的模式来构建协作流程。要避免并发的调用Add方法。这就带来一个问题,需要在一开始就能确定执行子任务的goroutine的数量,至少也是在启动goroutine之前。
下面是一个示例,稍微做了一些改造:

package main

import (
    "time"
    "fmt"
    "sync"
    "sync/atomic"
)

func coordinateWithWaitGroup() {
    total := 12
    var num int32
    var wg sync.WaitGroup
    // 定义好goroutine中返回前要执行的defer函数
    deferFunc := func() {
        wg.Done()
    }
    for i := 0; i < total; i++ {
        wg.Add(1)
        go addNum(&num, i, deferFunc)
    }
    wg.Wait()
}

// 这个函数的defer函数通过参数来给出
func addNum(numP *int32, id int, deferFunc func()) {
    defer deferFunc()
    for i := 1; ; i++ {
        currNum := atomic.LoadInt32(numP)
        newNum := currNum + 1
        time.Sleep(time.Millisecond * 200)
        if atomic.CompareAndSwapInt32(numP, currNum, newNum) {
            fmt.Printf("id: %02d 第 %02d 次更新num成功: %d\n", id, i, newNum)
            break
        }
    }
}

func main() {
    coordinateWithWaitGroup()
}

这里的改造是为了更像之后要使用context包时的用法,不过在使用规则上还是满足WaitGroup的要求的。

通过context包实现协作流程

这里就是要在写一个coordinateWithWaitContext函数,来代替上面的coordinateWithWaitGroup函数。两个函数要具有相同的功能。
这里先直接给出示例代码了:

func coordinateWithWaitContext() {
    total := 12
    var num int32
    cxt, cancelFunc := context.WithCancel(context.Background())
    // 定义好goroutine中返回前要执行的defer函数,这里用到了上面的cancelFunc
    deferFunc := func() {
        if atomic.LoadInt32(&num) == int32(total) {
            cancelFunc()
        }
    }
    for i := 0; i < total; i++ {
        go addNum(&num, i, deferFunc)
    }
    <- cxt.Done()
}

所有的变化都在上面这个函数里了。这里先后调用了context.Background函数和context.WithCancel函数。得到了一个可撤销context.Context类型的值,赋值给了变量cxt。还有一个context.CancelFunc类型的撤销函数,赋值给了变量cancelFunc。
这里在判断goroutine执行完毕的依据是通过判断num里的值。一旦判断完成,就会调用之前准备好的cancelFunc函数,此时cxt.Done函数返回的通道就会接收到值,结束等待。

和WaitGroup的比较
WaitGroup需要事先知道所有goroutine的数量,而context这里更关心是否满足某个条件,一旦条件满足就可以退出。
这里我想提一下python,让我想到了python中的for循环和while循环。能用for循环就不要用while循环。使用while循环可能由于条件判断复杂了,造成条件永远无法满足而成了死循环。使用for循环的话就没有这个问题了。不过当循环的退出和数量没有关系时,只能用while循环了。
就好比WaitGroup,如果可以通过goroutine的数量判断,那么应该还是使用WaitGroup好。如果遇到结束条件和goroutine数量无关的时候,就只能用context了。

context.Context类型

context.Context类型,是在Go 1.7发布时才被加入到标准库的。而后,标准库中的很多其他代码包都为了支持它而进行了扩展,包括:os/exec包、net包、database/sql包、runtime/pprof包和runtime/trace包,等等。
之所以会收到众多代码包的积极支持,主要因为它是一种非常通用的同步工具。它的值不但可以任意的扩散,而且还可以被用来传递额外的信息和信号。就是Context类型可以提供一个代表上下文的值,之类值是并发安全的,也就是说它可以被传播给多个goroutine。

接口类型
Context最新实际是一个接口类型,在context包中实现该接口的所有私有类型,都是基于某个数据类型的指针类型。所以,如此传播并不会影响该类型值的功能和安全。

可繁衍的
Context类型的值是可以繁衍的,这意味着可以通过一个Context值产生出任意个子值。这些子值可以携带父值的属性和数据,也可以相应通过其父值传达的信号。如此,所有的Context值共同构成了一颗代表了上下文全貌的属性结构。树的根节点是一个已经在context包中预定义好的context值,它是全局唯一的。通过调用context.Background函数,就可以获取到它。

包内的函数
在context包中,包含了4个用于繁衍Context值的函数:

  • WithCancel,产生一个可撤销的parent的子值
  • WithDeadline,产生一个会定时撤销的parent的子值
  • WithTimeout,同上,也是定时撤销的parent的子值
  • WithValue,产生一个会携带额外数据的parent的子值

这些函数的第一个参数类型都是context.Context,而名称都为parent。顾名思义,这个位置上的参数对应的都是产生Context值的父值。

撤销信号在上下文树中的传播

context包中的WithCancel、WithDeadline和WithTimeout都是被用来基于给定的COntext值产生可撤销的子值的。

WithCancel
这个函数在被调用后,产生两个结果值。第一个是可撤销的Context值,第二个是用于触发撤销信号的函数。
撤销函数被调用后,对应的Context值会先关闭它内部的接收通道,通道关闭了接收该通道的操作就会立即返回,就是Done方法返回的那个通道。然后,它还会向它的所有子值传达撤销信号。这些子值如果还有子值,就会一级一级把撤销信号传递下去。最后,这个Context值会断开它与其父值之间的关联。

WithDeadline和WithTimeout
通过调用WithDeadline函数或者WithTimeout函数生成的Context值也是可撤销的。它们不但可以被手动撤销,还会依据在生成是给定的过期时间,自动地进行定时撤销。这里的定时撤销功能是借助它们内部的计时器来实现的。
当过期时间到达时,两种Context值的行为与手动撤销是的行为是几乎一致的,只是多了一步停止并释放掉内部的计时器。
WithDeadline和WithTimeout是相似的。都是通过设置,会在某个时间自动触发,就是ctx.Done()能够取到值。差别是,DeadLine是设置一个时间点,时间对上了就到期。Timeout是设置一段时间,比如几秒,过个这段时间,就超时。其实底层的Timeout也是通过Deadlin实现的。

WithValue
这个函数得到的值是不可撤销的。撤销信号在传播时,若遇到它们会直接跨过,并试图将信息直接传给它们的子值。

传递数据

通过WithValue函数产生新的Context值的时候需要3个参数:父值、键和值。这里键必须是可判断等的,类似字典的键。不过Context值并不是用字典来存储键和值的,而是简单地存储在父值相应的字段中。
通过Value方法,可以获取数据。在调用包含属性的Context值的Value方法是,会先判断给定的键,如有有就返回存储的值,否则会到其父值中继续查找,会一直沿着上下文根节点的方法一直查找。因为其他几种Context值都是无法携带数据的,所以Value方法在查找的时候,会跨过这这些Context值。

无法改变数据
Context接口没有提供改变数据的方法,所以通常只能通过在上下文数中添加含数据的Context值来存储新的数据,或者通过撤销此种值的父值丢弃掉相应的数据。如果存储在这里的数据可以从外部改变,那么必须自信保证安全。

下面这个示例展示了Context值里数据的传递:

package main

import (
    "context"
    "fmt"
    "time"
)

type myKey int

func main() {
    keys := []myKey{
        myKey(20),
        myKey(30),
        myKey(60),
        myKey(61),
    }
    values := []string{
        "value in node2",
        "value in node3",
        "value in node6",
        "value in node6Branch",
    }

    rootNode := context.Background()
    node1, cancelFunc1 := context.WithCancel(rootNode)
    defer cancelFunc1()

    node2 := context.WithValue(node1, keys[0], values[0])
    node3 := context.WithValue(node2, keys[1], values[1])
    fmt.Printf("The value of the key %v found in the node3: %v\n",
        keys[0], node3.Value(keys[0]))
    fmt.Printf("The value of the key %v found in the node3: %v\n",
        keys[1], node3.Value(keys[1]))
    fmt.Printf("The value of the key %v found in the node3: %v\n",
        keys[2], node3.Value(keys[2]))
    fmt.Println()

    node4, cancelFunc4 := context.WithCancel(node3)
    defer cancelFunc4()
    node5, cancelFunc5 := context.WithTimeout(node4, time.Hour)
    defer cancelFunc5()
    fmt.Printf("The value of the key %v found in the node5: %v\n",
        keys[0], node5.Value(keys[0]))
    fmt.Printf("The value of the key %v found in the node5: %v\n",
        keys[1], node5.Value(keys[1]))
    fmt.Println()

    node6 := context.WithValue(node5, keys[2], values[2])
    fmt.Printf("The value of the key %v found in the node6: %v\n",
        keys[0], node6.Value(keys[0]))
    fmt.Printf("The value of the key %v found in the node6: %v\n",
        keys[2], node6.Value(keys[2]))
    fmt.Println()

    node6Branch := context.WithValue(node5, keys[3], values[3])
    fmt.Printf("The value of the key %v found in the node6Branch: %v\n",
        keys[1], node6Branch.Value(keys[1]))
    fmt.Printf("The value of the key %v found in the node6Branch: %v\n",
        keys[2], node6Branch.Value(keys[2]))
    fmt.Printf("The value of the key %v found in the node6Branch: %v\n",
        keys[3], node6Branch.Value(keys[3]))
    fmt.Println()

    node7, cancelFunc7 := context.WithCancel(node6)
    defer cancelFunc7()
    node8, cancelFunc8 := context.WithTimeout(node7, time.Hour)
    defer cancelFunc8()
    fmt.Printf("The value of the key %v found in the node8: %v\n",
        keys[1], node8.Value(keys[1]))
    fmt.Printf("The value of the key %v found in the node8: %v\n",
        keys[2], node8.Value(keys[2]))
    fmt.Printf("The value of the key %v found in the node8: %v\n",
        keys[3], node8.Value(keys[3]))
}

总结

Context类型是一个可以实现多goroutine协作流程同步的工具。还可以通过它的值传达撤销信号或传递数据。
Context类型的值大体可分3种:

  • 根Context值
  • 可撤销的Context值
  • 含数据的Context值

所有的Context值共同构成了一颗上下文树。这棵树的作用域是全局的,根Context值就是树的根,它也是全局唯一的,并且不提供任何额外的功能。
包含数据的Context值不能被撤销,可撤销的Context值又无法携带数据。但是,由于它们共同组成了一个有机的整体,即上下文数,所以在功能上要比sync.WaitGroup强大的多。

这个系列偏重理论,就少了很多实际的应用,关于context包,我之前还有一篇:
https://blog.51cto.com/steed/2330218
在这篇里介绍了两个主要功能:

  • 控制超时时间
  • 保存上下文数据

网站标题:Go36-32-context.Context
转载注明:http://myzitong.com/article/ghspss.html