SoFunction
Updated on 2025-03-05

How to elegantly close coroutine methods in go language development

1. Introduction

This article will explain why you need to actively close it firstgoroutine, and explain how to close it in GogoroutineCommon routines include passing termination signals and coroutine internal capture termination signals. Afterwards, the article lists common scenarios where coroutines need to actively close their operation, such as starting a coroutine to perform a repetitive task. I hope that through the introduction of this article, readers can understand how to close it at the right time.goroutine, and learn about the closinggoroutinecommon routines.

2. Why do you need to close goroutine

2.1 The life cycle of coroutines

Understanding the life cycle of a coroutine is a prerequisite for gracefully closing a coroutine, because the current state of the coroutine needs to be known before closing a coroutine in order to take appropriate measures. So we need to understand it firstgoroutinelife cycle.

existGoIn the language, a coroutine is a lightweight thread that can run multiple coroutines at the same time in a program to improve the concurrency performance of the program. The life cycle of a coroutine includes three stages: creation, operation and ending.

First, you need to create a coroutine. The creation of the coroutine can be achieved through the keyword go, for example:

go func() {
    // Code executed by coroutines}()

The above code will start a new coroutine and execute anonymous functions in the new coroutine, and the coroutine has been created.

Once the coroutine is created, it will run in a new thread. The running status of a coroutine can be managed by the Go runtime (goroutine scheduler), which will automatically schedule the coroutine to the appropriate one.PRun in and ensure fair scheduling and balanced loads of coroutines.

During the run phase, the coroutine will continue to execute tasks until the task is completed or a termination condition is encountered. During the termination phase, the coroutine will be recycled, completing its entire life cycle.

To sum up, coroutines aregoThe keyword is started, and its business logic is executed in the coroutine until the termination condition is finally encountered. At this time, the task of the coroutine has ended and will enter the termination stage. The coroutine will eventually be recycled.

2.2 Coroutine Termination Conditions

Normally, after the coroutine task is completed, the coroutine will automatically exit, for example:

func main() {
   var wg 
   (1)
   go func() {
      defer ()
      // Code executed by coroutines      ("Coprocess execution completed")
   }()
   ()
   // Wait for the coroutine to complete execution   ("Main program ends")

In the above code, we useWaitGroupWait for the coroutine to complete execution. After the coroutine is executed, the program will output two messages: the coroutine is executed and the main program ends.

Another situation is that the coroutine occurs, and it will automatically exit. For example:

func main() {
    var wg 
    (1)
    go func() {
        defer ()
        // Code executed by coroutines        panic("An error occurred in coroutine")
    }()
    // Wait for the coroutine to complete execution    ()
    ("Main program ends")
}

In this case, the coroutine will also automatically exit and will no longer occupy system resources.

Overall, the termination condition of a coroutine is actually that the task execution in the coroutine is completed, or a panic occurs during the execution process. The coroutine will meet the termination condition and exit the execution.

2.3 Why do you need to actively close goroutine

Judging from the above coroutine termination conditions, under normal circumstances, as long as the coroutine completes the task normally, the coroutine will automatically exit, and there is no need to actively close it at this time.goroutine

Here we first give an example of a producer and a consumer. In this example, we create a producer and a consumer, and they pass through achannelConduct communication. Producer produces data and sends it to achannelIn this, consumerschannelRead data and process it. The code example is as follows:

func main() {
    // Producer code    go func(out chan<- int) {
        for i := 0; ; i++ {
            select {
            case out <- i:
                ("producer: produced %d\n", i)
            ()
        }
    }
    // Consumer logic    go func(in <-chan int) {
        for {
            select {
            case i := <-in:
                ("consumer: consumed %d\n", i)
            }
        }
    }
    // Let producer coroutines and consumer coroutines be executed forever    (100000000)
}

In this example, we use twogoroutine: Producers and consumers. ProducerchannelIn production data, consumers fromchannelconsumption data.

However, if there is a problem with the producer, the producer's coroutine will be exited and will no longer be executed. And the consumer is still waiting for the input of the data. At this time, there is no need for consumer coroutines to exist, but they actually need to exit the execution.

Therefore, for some coroutines that do not meet the termination conditions, they do not need to continue to execute. At this time, they actively close their execution to ensure the robustness and performance of the program.

3. How to close goroutine gracefully

Elegant closegoroutineWe can follow the following three steps to execute. First, it passes a signal to close the coroutine, second, the coroutine needs to be able to close the signal, and finally, when the coroutine exits, it can correctly release the resources it occupies. Through the above steps, you can stop gracefully when neededgoroutineexecution. The following is a detailed explanation of these three steps.

3.1 Passing a shutdown termination signal

First, by givinggoroutinePass a signal to close the coroutine, allowing the coroutine to exit. Can be used hereTo pass signals, the specific implementation can be calledWithCancel,WithDeadline,WithTimeoutetc. to create a cancel functionContextand call it when the coroutine needs to be closedCancelMethod toContextSend a cancel signal. The sample code is as follows:

ctx, cancel := (())
go func(ctx ) {
    for {
        select {
        // After calling the cancel function, you will be able to receive a notification here        case <-():
            return
        default:
            // do something
        }
    }
}(ctx)
// Call the cancel method to send a cancel signal when the coroutine needs to be closedcancel()

Here, when we want to terminate the execution of the coroutine, we only need to call to cancel it.contextThe object'sCancelMethods, the coroutine will be able to passcontextThe object receives a notification to terminate the execution of the coroutine.

3.2 Coroutine internal capture of termination signal

The coroutine also needs to be correctly captured when the signal is cancelled to be transmitted to the coroutine to terminate the process normally. Here we can useselectStatement to listen for cancel signal.selectThere can be multiple statementscaseclauses that can listen to multiplechannel,whenselectWhen the statement is executed, it will block until there is acaseThe clause can be executed.selectStatements can also contain default clauses. This clause will be executed when all case clauses cannot be executed, and is usually used to prevent blocking of select statements. as follows:

select {
case <-channel:
    // The channel has code executed when the data arrivesdefault:
    // Code executed when all channels have no data}

andcontextThe object'sDoneThe method also returns achannel, the cancel signal is through thischannelto pass. So we can inside the coroutine,selectStatement, in one ofcaseBranches to listen for cancel signals; use onedefaultBranches execute specific business logic in coroutines. When the termination signal has not arrived, the business logic is executed; after receiving the coroutine termination signal, the coroutine execution can also be terminated in time. as follows:

go func(ctx ) {
    for {
        select {
        // After calling the cancel function, you will be able to receive a notification here        case <-():
            return
        default:
            // Execute business logic        }
    }
}(ctx)

3.3 Recycling coroutine resources

Finally, when the coroutine is terminated, the occupied resources need to be released, including file handles, memory, etc., so that other programs can continue to use these resources. In Go language, you can usedeferStatement to ensure that coroutines can correctly release resources when exiting. For example, if a file is opened in the coroutine, you can close it through the defer statement to avoid resource leakage. The code example is as follows:

func doWork() {
    file, err := ("")
    if err != nil {
        (err)
    }
    defer ()
    // Do some work
}

In this example, we usedeferThe statement registers a function that is automatically called when the coroutine ends to close the file. In this way, whenever the coroutine exits, we can ensure that the file is closed correctly and avoid resource leakage and other problems.

3.4 Close goroutine example

Here is a simple example, combined withContextObject,selectStatements anddeferThe three parts of the statement are elegantly terminated by the operation of a coroutine. The specific code examples are as follows:

package main
import (
    "context"
    "fmt"
    "time"
)
func worker(ctx ) {
    // Finally, before the coroutine exits, release the resource.    defer ("worker stopped")
    for {
        // Monitor the cancel signal through the select statement. If the cancel signal fails to arrive, then execute the business logic and wait for the next loop check.        select {
        default:
            ("working")
        case <-():
            return
        }
        ()
    }
}
func main() {
    ctx, cancel := (())
    // Start a coroutine to execute tasks    go worker(ctx)
    // After 5 seconds of execution, call cancel function to terminate the coroutine    (5 * )
    cancel()
    (2 * )
}

existmainIn the function, we useThe function creates a newcontextand pass it toworkerFunctions, start coroutine running at the same timeworkerfunction.

whenworkerAfter the function is executed for 5 seconds, the main coroutine is calledcancelFunction to terminateworkerCoroutine. after,workerListen to cancel signals in coroutinesselectThe statement will be able to capture this signal and perform a terminated coroutine operation.

Finally, when exiting the coroutine,deferStatements realize the release of resources. In summary, we have achieved elegant closure of coroutines and also recycled resources correctly.

4. Common scenarios where coroutines need to be actively shut down

4.1 Coroutines are performing a repetitive task

When a coroutine executes a repetitive task, the coroutine will not proactively terminate its operation. However, after a certain moment, there is no need to continue to perform the task, and it is necessary to actively close itgoroutineThe execution of the coroutine is released.

HereetcdLet’s explain it as an example.etcdIt is mainly used to store configuration information, metadata and some small-scale shared data in distributed systems. That is, we canetcdSome key-value pairs are stored. So, if we want to set the validity period of key-value pairs, how should we achieve it?

etcdThere is one inleaseThe concept of , a lease can be regarded as a time period, in which the existence of a key-value pair is meaningful, but after the lease expires, the existence of the key-value pair is meaningless and can be deleted. At the same time, a lease can act on multiple key-value pairs. Here is an example of how to associate a lease with a key:

// client is the connection of etcd client, establish a Lease instance based on this// Lease example provides some APIs that can create a lease, cancel a lease, and renew a leaselease := (client)
// Create a lease, and the lease time is 10 secondsgrantResp, err := ((), 10)
if err != nil {
    (err)
}
// Lease ID, each lease has a unique IDleaseID := 
// Associate the lease with the key, the validity period of the key at this time, that is, the validity period of the lease_, err = ((), "key1", "value1", (leaseID))
if err != nil {
    (err)
}

The above code demonstrates how toetcdCreate a lease in and associate it with a key-value pair. First, byetcdThe client's connection creates aLeaseInstance, this instance provides a number of APIs that can create leases, cancel leases, and renew leases. Then useGrantThe function creates a lease and specifies that the lease has a validity period of 10 seconds. Next, get the lease ID, each lease has a unique ID. Finally, usePutThe function associates the lease with the key, thus setting the validity period of the key to the validity period of the lease.

So, if we want to operateetcdThe validity period of the middle key-value pair is only required to operate the validity period of the lease.

And just so,etcdActually defines aLeaseInterface, This interface defines some operations on a lease, can create a lease, cancel the lease, and also supports renewal of the lease, obtaining expiration time and other contents. The details are as follows:

type Lease interface {
   // 1. Create a new lease   Grant(ctx , ttl int64) (*LeaseGrantResponse, error)
   // 2. Cancel the lease   Revoke(ctx , id LeaseID) (*LeaseRevokeResponse, error)
   // 3. Obtain the remaining validity period of the lease   TimeToLive(ctx , id LeaseID, opts ...LeaseOption) (*LeaseTimeToLiveResponse, error)
   // 4. Obtain all leases   Leases(ctx ) (*LeaseLeasesResponse, error)
   // 5. Continuously renew the lease. Here it is assumed that it expires after 10 seconds. The general meaning at this time is to renew the lease every 10 seconds. After calling this method, the lease will never expire.   KeepAlive(ctx , id LeaseID) (<-chan *LeaseKeepAliveResponse, error)
   // 6. Renew the lease once   KeepAliveOnce(ctx , id LeaseID) (*LeaseKeepAliveResponse, error)
   // 7. Close the Lease instance   Close() error
}

So far, we introduceLeaseinterface, andKeepAliveThe method is our protagonist today. From the definition of this method, it can be seen that when calledKeepAliveMethod After renewing a lease, it will perform the renewal of the target lease every once in a while. At this time, a coroutine is usually started, and the coroutine is used to complete the lease renewal operation.

At this time, the coroutine is actually executing a constantly repeating task, ifLeaseThe interface instance has been calledCloseMethod, want to recycleLeaseThe instance will no longer operate on the lease and will be recycled.LeaseAll the resources occupied, thenKeepAliveThe coroutine created by the method should also be closed actively at this time and should not continue to be executed.

In fact, currentlyetcdmiddleLeaseIn the interfaceKeepAliveThe same is true for the default implementation of the method. And the implementation of the active shutdown of coroutine operation is also throughcontextPass the object,selectGet the cancel signal and finally passdeferThe combination of these three resources is achieved by recycling them.

Let’s take a look at the functions that perform contract renewal operations. A coroutine will be started and continuously executed in the background. The specific implementation is as follows:

func (l *lessor) sendKeepAliveLoop(stream pb.Lease_LeaseKeepAliveClient) {
   for {
      var tosend []LeaseID
      now := ()
      ()
      // keepAlives saves all lease IDs to be renewed      for id, ka := range  {
         // Then nextKeepAlive is the next renewal time. If this time is exceeded, the renewal operation will be performed.         if (now) {
            tosend = append(tosend, id)
         }
      }
      ()
      // Send a renewal request      for _, id := range tosend {
         r := &{ID: int64(id)}
         // Send a renewal request to the etcd cluster         if err := (r); err != nil {
            return
         }
      }
      select {
      // Execute every 500ms      case <-(500 * ):
      // If a termination signal is received, it will be terminated directly      case <-():
         return
      }
   }
}

It can be seen that it will continue to cycle. First, it will check whether the current time exceeds the next renewal time of all leases. If it exceeds, the IDs of these leases will be placed in.tosendIn the array and in the next step of the loop toetcdThe cluster sends a renewal request. Then wait 500 milliseconds and then perform the above again. Under normal circumstances, it will not exit the loop and will continue toetcdThe cluster sends a renewal request. It will exit unless it receives a termination signal, thus ending the coroutine normally.

andstopCtxYeslessorA variable of the instance, used to pass a cancel signal. CreatinglessorWhen an instance,stopCtxIt is by()Function created. This function returns two objects: one with a cancel methodObject (i.e.stopCtx), and a function objectstopCancel, calling this function will cancel the context object. The details are as follows:

// Create a Lease instancefunc NewLeaseFromLeaseClient(remote , c *Client, keepAliveTimeout ) Lease {
   // ...Omit some irrelevant content   reqLeaderCtx := WithRequireLeader(())
   // Create cancelCtx object through the withCancel function   ,  = (reqLeaderCtx)
   return l
}

exist()In the function, we callstopCancel()Function to send a cancel signal.

func (l *lessor) Close() error {
   ()
   // close for synchronous teardown if stream goroutines never launched
   // Omit irrelevant content   return nil
}

becausesendKeepAliveLoop()The coroutine will bestopCtxWaiting for signal, so once calledstopCancel(), the coroutine will receive a signal and exit. This mechanism is very flexible becausestopCtxis a member variable of the instance, solessorAll coroutines created by instances can be listened throughstopCtxto decide whether to exit execution.

5. Summary

This article mainly introduces why you need to close it activelygoroutine, and close in Gogoroutinecommon routines.

The article first introduces why it is necessary to actively close itgoroutine. Next, the article introduces in detailGoClose in languagegoroutineCommon routines include passing termination signals and coroutine internal capture termination signals. In the scheme of delivering a termination signal, the article describes how to use itcontextThe object passes signals and usesselectStatement waits for signal. In the scheme of capturing a termination signal inside the coroutine, the article describes how to use itdeferStatements to recycle resources.

Finally, the article lists common scenarios where coroutines need to actively close the execution of coroutines. For example, coroutines are executing a constantly repetitive task and no longer need to continue execution, coroutines need to actively close the execution. I hope that through the introduction of this article, readers can understand how to close it at the right time.goroutine, thereby avoiding the problem of waste of resources.

The above is the detailed content of how to elegantly close coroutine methods in go language development. For more information about go to close coroutines, please follow my other related articles!