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 main
import (
"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,每一个租约都有一个仅有的ID
leaseID := 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
,然后防止资源浪费的问题。