1.简介
本文将介绍首先为什么须要被动敞开goroutine
,并介绍如何在Go语言中敞开goroutine
的常见套路,包含传递终止信号和协程外部捕获终止信号。之后,文章列举了须要被动敞开协程运行的常见场景,如启动一个协程执行一个一直反复的工作。心愿通过本文的介绍,读者可能把握如何在适当的时候敞开goroutine
,以及理解敞开goroutine
的常见套路。
2.为什么须要敞开goroutine
2.1 协程的生命周期
理解协程的生命周期是优雅地敞开协程的前提,因为在敞开协程之前须要晓得协程的以后状态,以便采取相应的措施。所以这里咱们须要先理解下goroutine
的生命周期。
在 Go
语言中,协程(goroutine)是一种轻量级的线程,能够在一个程序中同时运行多个协程,进步程序的并发性能。协程的生命周期包含创立、运行和完结三个阶段。
首先须要创立一个协程,协程的创立能够通过关键字 go 来实现,例如:
go func() { // 协程执行的代码}()
下面的代码会启动一个新的协程,同时在新的协程中执行匿名函数,此时协程便已被创立了。
一旦协程被创立,它就会在新的线程中运行。协程的运行状态能够由 Go 运行时(goroutine scheduler)来治理,它会主动将协程调度到适当的P
中运行,并确保协程的偏心调度和均衡负载。
在运行阶段,协程会一直地执行工作,直到工作实现或者遇到终止条件。在终止阶段,协程将会被回收,从而实现其整个生命周期。
综上所述,协程由go
关键字启动,在协程中执行其业务逻辑,直到最初遇到终止条件,此时代表着协程的工作曾经完结了,将进入终止阶段。最终协程将会被回收。
2.2 协程的终止条件
失常来说,都是协程工作执行实现之后,此时协程主动退出,例如:
func main() { var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() // 协程执行的代码 fmt.Println("协程执行结束") }() wg.Wait() // 期待协程执行结束 fmt.Println("主程序完结")
下面的代码中,咱们应用 WaitGroup
期待协程执行结束。在协程执行结束后,程序会输入协程执行结束和主程序完结两条信息。
还有一种状况是协程产生panic,它将会主动退出。例如:
func main() { var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() // 协程执行的代码 panic("协程产生谬误") }() // 期待协程执行结束 wg.Wait() fmt.Println("主程序完结")}
在这种状况下,协程也会主动退出,不会再占用系统资源。
综合看来,协程的终止条件,其实就是协程中的工作执行实现了,或者是执行过程中产生了panic,协程将满足终止条件,退出执行。
2.3 为什么须要被动敞开goroutine
从下面协程的终止条件来看,失常状况下,协程只有将工作失常解决实现,协程主动退出,此时并不需要被动敞开goroutine
。
这里先举一个生产者消费者的例子,在这个例子中,咱们创立了一个生产者和一个消费者,它们之间通过一个channel
进行通信。生产者生产数据并发送到一个channel
中,消费者从这个channel
中读取数据并进行解决。代码示例如下:
func main() { // 生产者代码 go func(out chan<- int) { for i := 0; ; i++ { select { case out <- i: fmt.Printf("producer: produced %d\n", i) time.Sleep(time.Second) } } // 消费者逻辑 go func(in <-chan int) { for { select { case i := <-in: fmt.Printf("consumer: consumed %d\n", i) } } } // 让生产者协程和消费者协程始终执行上来 time.Sleep(100000000)}
在这个例子中,咱们应用了两个goroutine
:生产者和消费者。生产者向channel
中生产数据,消费者从channel
中生产数据。
然而,如果生产者呈现了问题,此时生产者的协程将会被退出,不再执行。而消费者依然在期待数据的输出。此时消费者协程曾经没有存在的必要了,其实是须要退出执行。
因而,对于一些尽管没有达到终止条件的协程,然而其又没有再继续执行上来的必要,此时被动敞开其执行,从而保障程序的健壮性和性能。
3.如何优雅得敞开goroutine
优雅得敞开goroutine
的执行,咱们能够遵循以下三个步骤。首先是传递敞开协程的信号,其次是协程外部须要可能到敞开信号,最初是协程退出时,可能正确开释其所占据的资源。通过以上步骤,能够保在须要时优雅地进行goroutine
的执行。上面对这三个步骤具体进行解说。
3.1 传递敞开终止信号
首先是通过给goroutine
传递敞开协程的信号,从而让协程进行退出操作。这里能够应用context.Context
来传递信号,具体实现能够通过调用WithCancel
,WithDeadline
,WithTimeout
等办法来创立一个带有勾销性能的Context
,并在须要敞开协程时调用Cancel
办法来向Context
发送勾销信号。示例代码如下:
ctx, cancel := context.WithCancel(context.Background())go func(ctx context.Context) { for { select { // 调用cancel函数后,这里将可能收到告诉 case <-ctx.Done(): return default: // do something } }}(ctx)// 在须要敞开协程时调用cancel办法发送勾销信号cancel()
这里,当咱们想要终止协程的执行时,只须要调用可勾销context
对象的Cancel
办法,协程外部将可能通过context
对象接管到终止协程执行的告诉。
3.2 协程外部捕获终止信号
协程外部也须要在勾销信号传递过去时,可能正确被捕捉到,才可能失常终止流程。这里咱们能够应用select
语句来监听勾销信号。select
语句能够有多个case
子句,能够同时监听多个channel
,当select
语句执行时,它会始终阻塞,直到有一个case
子句能够执行。select
语句也能够蕴含default子句,这个子句在所有的case子句都不能执行时会被执行,通常用于避免select语句的阻塞。如下:
select {case <-channel: // channel有数据到来时执行的代码default: // 所有channel都没有数据时执行的代码}
而context
对象的Done
办法刚好也是返回一个channel
,勾销信号便是通过该channel
来进行传递的。所以咱们能够在协程外部,通过select
语句,在其中一个case
分支来监听勾销信号;同时应用一个default
分支在协程中执行具体的业务逻辑。在终止信号没有到来时,就执行业务逻辑;在收到协程终止信号后,也可能及时终止协程的执行。如下:
go func(ctx context.Context) { for { select { // 调用cancel函数后,这里将可能收到告诉 case <-ctx.Done(): return default: // 执行业务逻辑 } }}(ctx)
3.3 回收协程资源
最初,当协程被终止执行时,须要开释占用的资源,包含文件句柄、内存等,以便其余程序能够持续应用这些资源。在Go语言中,能够应用defer
语句来确保协程在退出时可能正确地开释资源。比方协程中关上了一个文件,此时能够通过defer语句来敞开,防止资源的透露。代码示例如下:
func doWork() { file, err := os.Open("test.txt") if err != nil { log.Fatal(err) } defer file.Close() // Do some work}
在这个例子中,咱们在文件关上之后应用defer
语句注册了一个函数,当协程完结时会主动调用该函数来敞开文件。这样协程无论在何时退出,咱们都能够确保文件被正确敞开,防止资源透露和其余问题。
3.4 敞开goroutine示例
上面展现一个简略的例子,联合Context
对象,select
语句以及defer
语句这三局部内容,优雅得终止一个协程的运行,具体代码示例如下:
package mainimport ( "context" "fmt" "time")func worker(ctx context.Context) { // 最初,在协程退出前,开释资源. defer fmt.Println("worker stopped") for { // 通过select语句监听勾销信号,勾销信号没达到,则执行业务逻辑,等下次循环查看 select { default: fmt.Println("working") case <-ctx.Done(): return } time.Sleep(time.Second) }}func main() { ctx, cancel := context.WithCancel(context.Background()) // 启动一个协程执行工作 go worker(ctx) // 执行5s后,调用cancel函数终止协程 time.Sleep(5 * time.Second) cancel() time.Sleep(2 * time.Second)}
在main
函数中,咱们应用context.WithCancel
函数创立了一个新的context
,并将其传递给worker
函数,同时启动协程运行worker
函数。
当worker
函数执行5s后,主协程调用cancel
函数来终止worker
协程。之后,worker
协程中监听勾销信号的select
语句,将可能捕捉到这个信号,执行终止协程操作。
最初,在退出协程时,通过defer
语句实现资源的开释。综上,咱们实现了协程的优雅敞开,同时也正确回收了资源。
4. 须要被动敞开协程运行的常见场景
4.1 协程在执行一个一直反复的工作
协程在执行一个一直反复的工作时,此时协程是不会被动终止运行的。然而在某个时刻之后,不须要再继续执行该工作了,须要被动敞开goroutine
的执行,开释协程的资源。
这里以etcd
为例来进行阐明。etcd
次要用于在分布式系统中存储配置信息、元数据和一些小规模的共享数据。也就是说,咱们能够在etcd
当中存储一些键值对。那么,如果咱们想要设置键值对的有效期,那该如何实现呢?
etcd
中存在一个租约的概念,租约能够看作是一个时间段,该时间段内某个键值对的存在是有意义的,而在租约到期后,该键值对的存在便没有意义,能够被删除,同时一个租约能够作用于多个键值对。上面先展现如何将一个租约和一个key进行关联的示例:
// client 为 etcd客户端的连贯,基于此建设一个Lease实例// Lease示例提供一些api,能过创立租约,勾销租约,续约租约lease := clientv3.NewLease(client)// 创立一个租约,同时租约工夫为10秒grantResp, err := lease.Grant(context.Background(), 10)if err != nil { log.Fatal(err)}// 租约ID,每一个租约都有一个惟一的IDleaseID := grantResp.ID// 将租约与key进行关联,此时该key的有效期,也就是该租约的有效期_, err = kv.Put(context.Background(), "key1", "value1", clientv3.WithLease(leaseID))if err != nil { log.Fatal(err)}
以上代码演示了如何在etcd
中创立一个租约并将其与一个键值对进行关联。首先,通过etcd
客户端的连贯创立了一个Lease
实例,该实例提供了一些api,能够创立租约、勾销租约和续约租约。而后应用Grant
函数创立了一个租约并指定了租约的有效期为10秒。接下来,获取租约ID,每个租约都有一个惟一的ID。最初,应用Put
函数将租约与key进行关联,从而将该key的有效期设定为该租约的有效期。
所以,咱们如果想要操作etcd
中键值对的有效期,只须要操作租约的有效期即可。
而刚好,etcd
其实定义了一个Lease
接口,该接口定义了对租约的一些操作,能过创立租约,勾销租约,同时也反对续约租约,获取过期工夫等内容,具体如下:
type Lease interface { // 1. 创立一个新的租约 Grant(ctx context.Context, ttl int64) (*LeaseGrantResponse, error) // 2. 勾销租约 Revoke(ctx context.Context, id LeaseID) (*LeaseRevokeResponse, error) // 3. 获取租约的残余有效期 TimeToLive(ctx context.Context, id LeaseID, opts ...LeaseOption) (*LeaseTimeToLiveResponse, error) // 4. 获取所有的租约 Leases(ctx context.Context) (*LeaseLeasesResponse, error) // 5. 一直对租约进行续约,这里假如10s后过期,此时大略的含意为每隔10s续约一次租约,调用该办法后,租约将永远不会过期 KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error) // 6. 续约一次租约 KeepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAliveResponse, error) // 7. 敞开Lease实例 Close() error}
到此为止,咱们引出了Lease
接口,而其中KeepAlive
办法便是咱们今日的配角,从该办法定义能够看出,当调用KeepAlive
办法对某个租约进行续约后,其每隔一段时间都会执行对指标租约的续约操作。这个时候个别都是启动一个协程,由协程来实现对租约的续约操作。
此时协程其实就是在执行一个一直反复的工作,那如果Lease
接口的实例调用了Close
办法,想要回收掉Lease
实例,不会再通过该实例对租约进行操作,回收掉Lease
所有占据的资源,那么KeepAlive
办法创立的协程,此时也应该被被动敞开,不应该再继续执行上来。
事实上,以后etcd
中Lease
接口中KeepAlive
办法的默认实现也是如此。并且对被动敞开协程运行的实现,也是通过context
传递对象,select
获取勾销信号,最初通过defer
来回收资源这三者组合起来实现的。
上面来看看执行续约操作的函数,会启动一个协程在后盾一直执行,具体实现如下:
func (l *lessor) sendKeepAliveLoop(stream pb.Lease_LeaseKeepAliveClient) { for { var tosend []LeaseID now := time.Now() l.mu.Lock() // keepAlives 是保留了所有待续约的 租约ID for id, ka := range l.keepAlives { // 而后nextKeepAlive为下次续约的工夫,如果超过该工夫,则执行续约操作 if ka.nextKeepAlive.Before(now) { tosend = append(tosend, id) } } l.mu.Unlock() // 发送续约申请 for _, id := range tosend { r := &pb.LeaseKeepAliveRequest{ID: int64(id)} // 向etcd集群发送续约申请 if err := stream.Send(r); err != nil { return } } select { // 每隔500ms执行一次 case <-time.After(500 * time.Millisecond): // 如果接管到终止信号,则间接终止 case <-l.stopCtx.Done(): return } }}
能够看到,其会一直循环,首先会查看以后工夫是否超过了所有租约的下次续约工夫,如果超过了,则会将这些租约的 ID 放入 tosend
数组中,并在循环的下一步中向 etcd
集群发送续约申请。接着会期待 500 毫秒,而后再次执行上述操作。失常状况下,其不会退出循环,会始终向etcd
集群发送续约申请。除非收到了终止信号,其才会退出,从而失常完结协程。
而stopCtx
则是lessor
实例的变量,用于传递勾销信号。在创立 lessor
实例时,stopCtx
是由 context.WithCancel()
函数创立的。这个函数会返回两个对象:一个带有勾销办法的 context.Context
对象(即 stopCtx
),以及一个函数对象 stopCancel
,调用这个函数会勾销上下文对象。具体如下:
// 创立Lease实例func NewLeaseFromLeaseClient(remote pb.LeaseClient, c *Client, keepAliveTimeout time.Duration) Lease { // ...省略一些无关内容 reqLeaderCtx := WithRequireLeader(context.Background()) // 通过withCancel函数创立cancelCtx对象 l.stopCtx, l.stopCancel = context.WithCancel(reqLeaderCtx) return l}
在 lessor.Close()
函数中,咱们调用 stopCancel()
函数来发送勾销信号。
func (l *lessor) Close() error { l.stopCancel() // close for synchronous teardown if stream goroutines never launched // 省略无关内容 return nil}
因为 sendKeepAliveLoop()
协程会在 stopCtx
上期待信号,所以一旦调用了 stopCancel()
,协程会收到信号并退出。这个机制非常灵活,因为stopCtx
是实例的成员变量,所以lessor
实例创立的所有协程,都能够通过监听stopCtx
来决定是否要退出执行。
5.总结
这篇文章次要介绍了为什么须要被动敞开goroutine
,以及在Go语言中敞开goroutine
的常见套路。
文章首先介绍了为什么须要被动敞开goroutine
。接下来,文章具体介绍了Go
语言中敞开goroutine
的常见套路,包含传递终止信号和协程外部捕获终止信号。在传递终止信号的计划中,文章介绍了如何应用context
对象传递信号,并应用select
语句期待信号。在协程外部捕获终止信号的计划中,文章介绍了如何应用defer
语句来回收资源。
最初,文章列举了须要被动敞开协程运行的常见场景,如协程在执行一个一直反复的工作,在不再须要继续执行上来的话,就须要被动敞开协程的执行。心愿通过本文的介绍,读者可能把握如何在适当的时候敞开goroutine
,从而防止资源节约的问题。