随笔 - 241  文章 - 1  评论 - 58  阅读 - 85万 

前言

在Golang中main函数是程序执行的入口和主线,无论怎样,main函数早于它开启的Goroutines结束执行都不属于正常的程序执行流程

使用以下3种方式,可以控制Goroutines的执行顺序

1.sync.WaitGroup

main函数等待,开启的子Goroutines正常执行完毕。

复制代码
package main

import (
    "fmt"
    "golang.org/x/sys/windows"
    "sync"
)

var naturalNumberCh = make(chan int, 100)
var wg sync.WaitGroup
func write() {
    threadID := windows.GetCurrentThreadId()
    defer func() {
        wg.Done()
        //记得channel写入完成关闭,否则for range循环读一直不结束!
        close(naturalNumberCh)
        fmt.Printf("----write-%d结束\n", threadID)
    }()
    for i := 0; i <= 100; i++ {
        naturalNumberCh <- i
    }

}

func read() {
    defer wg.Done()
    threadID := windows.GetCurrentThreadId()
    //记得写完了关闭Channel,否则for range循环不结束!
    for n := range naturalNumberCh {
        fmt.Printf("----read-goroutine-%d读取到值%d\n", threadID, n)
    }
}

func main() {
    defer fmt.Println("mian函数结束")
    readerCount := 3
    writeCount := 1
    //开1个写go程
    for i := 0; i < writeCount; i++ {
        go write()

    }
    //开3个读go程执行结束后主动通知main函数,发请求结束的请求!
    for i := 0; i < readerCount; i++ {
        go read()
    }
    gocount := readerCount + writeCount
    wg.Add(gocount)
    wg.Wait()
}
View Code
复制代码

2.channel

main函数等待开启的子Goroutines正常执行完毕,利用channel的阻塞特性+约定channel传输值的个数机制,

复制代码
package main

import (
    "fmt"
    "golang.org/x/sys/windows"
)

var naturalNumberCh = make(chan int, 100)
var readDoneCh = make(chan bool, 3)

func write() {
    threadID := windows.GetCurrentThreadId()
    defer func() {
        //记得channel写入完成关闭,否则for range循环读一直不结束!
        close(naturalNumberCh)
        fmt.Printf("----write-%d结束\n", threadID)
    }()
    for i := 0; i <= 100; i++ {
        naturalNumberCh <- i
    }

}

func read() {
    threadID := windows.GetCurrentThreadId()
    //记得写完了关闭Channel,否则for range循环不结束!
    for n := range naturalNumberCh {
        fmt.Printf("----read-goroutine-%d读取到值%d\n", threadID, n)
    }
    readDoneCh <- true
}

func main() {
    defer fmt.Println("mian函数结束")
    readerCount := 3
    writeCount := 1
    //开1个写go程
    for i := 0; i < writeCount; i++ {
        go write()

    }
    //开3个读go程执行结束后主动通知main函数,发请求结束的请求!
    for i := 0; i < readerCount; i++ {
        go read()
    }

    //等待3个读go程都执行完毕了,main函数再结束
    for i := 0; i < readerCount; i++ {
        fmt.Println(<-readDoneCh)
    }

}
View Code
复制代码

3.context

main函数主动终止开启的子Goroutines,子Goroutines接收到终止信号,被动退出。

复制代码
package main

import (
    "context"
    "fmt"
    "golang.org/x/sys/windows"
    "time"
)

var naturalNumberCh = make(chan int, 100)

func write() {
    threadID := windows.GetCurrentThreadId()
    defer func() {
        //记得channel写入完成关闭,否则for range循环读一直不结束!
        close(naturalNumberCh)
        fmt.Printf("----write-%d结束\n", threadID)
    }()
    for i := 0; i <= 100; i++ {
        naturalNumberCh <- i
    }

}

func read(ctx context.Context) {
    threadID := windows.GetCurrentThreadId()
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("----read-goroutine-%d需要强制结束!\n", threadID)
            return
        default:
            //记得写完了关闭Channel,否则for range循环不结束!
            for n := range naturalNumberCh {
                fmt.Printf("----read-goroutine-%d读取到值%d\n", threadID, n)
            }

        }
    }

}

func main() {
    defer fmt.Println("mian函数结束")
    readerCount := 3
    writeCount := 1
    ctx, cancel := context.WithCancel(context.Background())
    //开1个写go程
    for i := 0; i < writeCount; i++ {
        go write()

    }
    //开3个读go程执行结束后主动通知main函数,发请求结束的请求!
    for i := 0; i < readerCount; i++ {
        go read(ctx)
    }
    //等读go程读取完成
    time.Sleep(5 * time.Second)
    cancel()
    //等读go程全部关闭
    time.Sleep(5 * time.Second)

}
View Code
复制代码
如有父goroutine在后台启动了1个goroutine(日志采集模块一直taill日志文件的内容是否新增然后发送到kafka),父goroutine突然得知这个日志路径变了。
由于开启的这个日志采集子goroutine是在后台一直执行的......总不能重启线上服务/重新加载配置更不能os.Exit(),那么父goroutine如何让这个一直忙着干活的子goroutine退出呢
我们就可以让子goroutine携带1个Context,子goroutine携带了这个Context,父goroutine就可以通过这个Context达到跟踪、退出子goroutines的目的。
以上2种控制方式虽然可以实现Goroutine的控制,但是不是一种Golang开发者公认的标准;
Go语言官方提供了Context包,开发者可以把context作为1种Goroutine控制标准
context官方提供用于帮助我们以一种优雅的方式通过父goroutine追踪、退出无法自行退出的子goroutines内置包

问题

当1个子goroutine已经被开启的时候,我们如何在不结束自己(父goroutine)的前提下,结束这个衍生的子goroutine呢?
package main
 
import (
    "fmt"
    "time"
)
 
import (
    "sync"
)
 
var wg sync.WaitGroup
 
 
//toSchool 子goroutine
func toSchool() {
    defer wg.Done()
    for {
        fmt.Println("walking~~~~")
        time.Sleep(time.Second * 1)
    }
}
 
func main() {
    wg.Add(1)
    //当1个子goroutine开启的时候....
    go toSchool()
    fmt.Println("台风来了!")
    //我们如何在不结束自己的前提下结束这些衍生的子goroutine呢?
    wg.Wait()
}

  

自由解决方案

既然是context是golang官方提出的标准方案,相对而言也会有自由解决方案。

1.通过全局变量控制子goroutine退出

  

  

2.通过全局channel控制goroutine退出

or

  

 

退出和追踪衍生goroutines的方式有很多,我们不使用go内置的context也能完全解决这一问题,但是每个程序员使用的解决方案不同的话,就会增加代码的阅读难度。

 

 

官方解决方案(context)

Package context defines the Context type, which carries deadlines, cancelation signals, and other request-scoped values across API boundaries and between processes.

Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context.

The chain of function calls between them must propagate the Context,

optionally replacing it with a derived Context

created using WithCancel, WithDeadline, WithTimeout, or WithValue.

When a Context is canceled, all Contexts derived from it are also canceled.

 

什么是context?

context.Context是一个接口,该接口定义了四个需要实现的方法。具体签名如下:

1
2
3
4
5
6
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

 

context可以定义Context类型,专门用来简化 对于处理1个请求的N个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。

 

创建使用context

 

Background()和TODO() (根节点)

Go内置两个函数:Background()TODO(),这两个函数分别返回一个实现了Context接口的backgroundtodo。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象。

Background()主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context

TODO(),它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。(必须要传递1个context类型的参数)

todo本质上也是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context

 

WithCancel(取消/退出)
主要用在父goroutine,控制子goroutine退出。
cancel closes c.done, cancels each of c's children, and, if removeFromParent is true, removes c from its parent's children.
NOTE: acquiring the child's lock while holding parent's lock.
特性:一旦 根节点的goroutine执行cancel() 关闭的时候,它的所有后代都将被关闭。
 
ps:
可以把WithCancel这种context和goroutine一起封装到同1个struct里面。your kown for canceling.
1
2
3
4
5
6
7
8
9
//1个具体的日志收集任务(TaillTask)
type TaillTask struct {
    path     string
    topic    string
    instance *tail.Tail
    //exit task
    ctx  context.Context
    exit context.CancelFunc
}

more

  

 

  

案例2

 

WithDeadline(绝对超时时间)

当context的截止日过期时, ctx.Done()返回后context deadline exceeded。

 

WithTimeout(相对超时时间)
和WithDeadline 是一对,过期之后context超时context deadline exceeded
 

 

WithValue(传递值)

WithCancel、WithDeadline、WithTimeout,With 这个verb 就是context可以追溯和退出其衍生子goroutines 的关键所在! 在子goroutine开启时就与生俱来一些元数据!

WithValue可以1个gorutin繁衍了N代子goroutines之后,它的后代goroutines都能with(携带)1个固定值,这样我就可以自上而下追溯这个繁衍链了!

这也是微服务链路追踪的核心思想。

1
func WithValue(parent Context, key, val interface{}) Context

 

WithValue returns a copy of parent in which the value associated with key is val.

WithValue返回父节点的副本,其中与key关联的值为val。

Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.

仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。我把它当成session来看!

The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context.

所提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用上下文在包之间发生冲突。

Users of WithValue should define their own types for keys. To avoid allocating when assigning to an interface{}, context keys often have concrete type struct{}. Alternatively, exported context key variables' static type should be a pointer or interface.

WithValue的用户应该为键定义自己的类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}。或者,导出的上下文关键变量的静态类型应该是指针或接口

 

context应用场景(微服务链路追踪)

作为1个微服务架构,微服务之间session不共享的服务端,如何追踪客户端1次request都调用了哪些微服务组件?并且聚合日志。

 

 

 

 

参考

posted on   Martin8866  阅读(486)  评论(0编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
历史上的今天:
2017-05-06 ATM
点击右上角即可分享
微信分享提示