Por qué es necesario el control del tiempo de espera

1. Introducción

Este artículo presentará por qué se necesita el control de tiempo de espera y luego presentará en detalle el método para implementar el control de tiempo de espera en el lenguaje Go. Entre ellos, discutiremos timeel paquete y contextla forma específica en que el paquete implementa el control de tiempo de espera, y explicaremos los escenarios aplicables de los dos, para que el control de tiempo de espera se pueda implementar de una manera más adecuada en el programa, y ​​la estabilidad y confiabilidad. del programa se puede mejorar.

2. Por qué es necesario el control del tiempo de espera

El control del tiempo de espera puede ayudarnos a evitar que el programa espere indefinidamente a que se complete una operación o evitar que el programa cause fugas de recursos por algún motivo. Específicamente, el control de tiempo de espera tiene las siguientes ventajas:

  1. Evite esperar indefinidamente : si una operación necesita esperar un período de tiempo, pero si no se completa después de este tiempo, es posible que el programa deba dejar de esperar y tomar otras medidas, como devolver un error o cancelar la operación. El procesamiento de tiempo de espera puede evitar que el programa espere indefinidamente, lo que mejora la solidez y la confiabilidad del programa.
  2. Prevenir fugas de recursos : si una determinada operación del programa ha estado en estado de espera, puede causar que los recursos estén ocupados todo el tiempo, lo que resulta en fugas de recursos. El control de tiempo de espera permite que el programa complete las operaciones dentro de un cierto período de tiempo y libere recursos si se produce un tiempo de espera, evitando así fugas de recursos.
  3. Mejore la capacidad de respuesta del programa : algunas operaciones pueden tardar mucho tiempo en completarse, lo que puede hacer que el programa deje de responder mientras espera. El control de tiempo de espera permite que el programa complete las operaciones dentro de un cierto período de tiempo y tome otras medidas si se agota el tiempo, mejorando así la velocidad de respuesta del programa.

Con base en las razones anteriores, podemos ver que en algunos escenarios, el control del tiempo de espera es esencial durante la ejecución del programa. Entonces, en Goel lenguaje, ¿cuáles son las formas de lograr el control del tiempo de espera?

3. Método de control de tiempo de espera

 3.1 paquete de tiempo implementa control de tiempo de espera    

timeEl paquete proporciona una variedad de formas de implementar el control de tiempo de espera, incluidas time.Afterfunciones, time.NewTimerfunciones y time.AfterFuncfunciones, que se pueden usar para implementar el control de tiempo de espera. A continuación, se usan time.NewTimerfunciones como ejemplo para ilustrar cómo usar su timepaquete para implementar el control de tiempo de espera. El ejemplo de código es el siguiente:

// 创建一个定时器
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()

// 使用一个channel来监听任务是否已完成
ch := make(chan string, 1)     
go func() {         
// 模拟任务执行,休眠5秒         
    time.Sleep(2* time.Second)         
    ch <- "hello world"     
}()

// 通过select语句来等待结果,任务正常返回
select {
case <-ch:
    fmt.Println("任务正常完成")
  // ch 已经接收到值,走正常处理逻辑
case <-timer.C:
    fmt.Println("已超时")
  // 超时,走超时逻辑
}
复制代码

在这里例子中,我们使用 time.NewTimer 方法创建一个定时器,超时时间为2秒钟。然后在 select 语句中使用来等待结果,哪个先返回就使用哪个。

如果操作在2秒钟内完成,那么任务正常完成;如果操作超过2秒钟仍未完成,此时select语句中<-timer.C将接收到值,走超时处理逻辑。

3.2 context实现超时控制

Context 接口是 Go 语言标准库中提供的一个上下文(Context)管理机制。它允许在程序的不同部分之间传递上下文信息,并且可以通过它实现超时控制、取消操作以及截断操作等功能。其中,Context接口存在一个timerCtx的实现,其可以设定一个超时时间,在到达超时时间后,timerCtx对象的 done channel 将会被关闭。

当需要判断是否超时时,只需要调用 context 对象的 Done 方法,其会返回timerCtx对象中的done channel,如果有数据返回,则说明已经超时。基于此,我们便可以实现超时控制。代码示例如下:

// 创建一个timerCtx,设置超时时间为3秒     
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)     
// 调用cancel函数,释放占用的资源  
defer cancel()

// 使用一个channel来监听任务是否已完成
ch := make(chan string, 1)     
go func() {         
// 模拟任务执行,休眠5秒         
    time.Sleep(2* time.Second)         
    ch <- "hello world"     
}()

// 通过select语句来等待结果,任务正常返回
select {
    case <-ctx.Done():
        fmt.Println("timeout")
    case result := <-ch:
        fmt.Println(result)
}
复制代码

这里通过context.WithTimeout创建一个timerCtx,设定好超时时间,超时时间为3s。然后启动一个协程来执行具体的业务逻辑。

之后通过select语句,对timerCtx和业务执行结果同时进行监听,当任务处理超时时,则执行超时逻辑;如果任务在超时前完成,则执行正常处理流程。通过这种方式,实现了请求的超时处理。

4. 适用场景分析

从上文可以看出,timetimerCtx都可以用于实现超时控制,但是事实上两者的适用场景其实是不太相同的。在某些场景下,超时控制并不适合使用time来实现,而是使用timerCtx来实现更为合适。而在某些场景下,其实两种实现方式均可。

下面我简单介绍几种常见的场景,然后对其来进行分析,从而能够在合适的场景下使用恰当得实现。

4.1 简单超时控制

举个例子,假设我们需要从一个远程服务获取一些数据,我们可以使用Go标准库中的http包进行网络请求,大概请求函数如下:

func makeRequest(url string) (string, error) {
   // 请求数据
}
复制代码

此时为了避免请求响应时间过长,导致程序长时间处于等待状态,此时我们需要对这个函数实现超时处理,确保程序能够及时响应其他请求,而不是一直等待。

为了实现这个目的,此时可以使用time包或者timerCtx来实现超时控制。在makeRequest函数中实现超时控制,这里代码展示与第三点超时控制的方法中的代码示例大体相同,这里不再赘述。而且,查看上面代码示例,我们也可以看出来timer或者timerCtx在这个场景下,区别并不大,此时是可以相互替换的。

因此,对于这种控制某个函数的执行时间的场景,是可以任意挑选time或者timerCtx其中一个来实现的。

4.2 可选超时控制

这里我们实现一个方法,用于建立网络连接,用户调用该方法时,传入待建立连接的地址列表,然后该方法通过遍历传入的地址列表,并针对每一个地址进行连接尝试,直到连接成功或者所有地址都尝试完成。函数定义如下:

func dialSerial(ras addrList) (Conn, error){
   // 执行建立网络连接的逻辑
}
复制代码

基于此,在这个函数的基础上,实现一个可选的超时控制的功能。如果用户调用该方法时,有指定超时时间的话,此时便进行超时控制;如果未指定超时时间的话,此时便无需执行超时控制。这里分别使用time包以及context实现。

首先对于time包实现可选的超时控制,可以通过函数参数传递定时器来实现可选的超时控制。具体地说,可以将定时器作为一个time.Timer类型的参数传递给函数,然后在函数中使用select监听time.Timer是超时;如果没有传递定时器实例,则默认不进行超时控制,代码实现如下所示:

func dialSerial(timeout time.Timer, ras addrList) (Conn, error){
   // 执行建立网络连接的逻辑,对每个地址尝试建立连接时,先检查是否超时
   for i, ra := range ras {
          // 通过这里来进行超时控制,首先先判断是否传入定时器实例
          if timeout != nil {
              select {
              // 监听是否超时
              case <-timeout.C:
                  return nil, errors.New("timeout")
              default:
              }
          }
         // 执行后续建立网络连接的逻辑          
   }
}
复制代码

接着则是使用timerCtx来实现超时控制的实现,可以通过函数传递一个context.Context接口的参数来实现超时控制。

具体来说,用户可以传递一个context.Context接口的实现,如果有指定超时时间,则传入一个timerCtx的实现;如果无需超时控制,此时可以传入context.Background,其永远不会超时。然后函数中通过调用Done方法来判断是否超时,从而实现超时控制。代码实现如下:

func dialSerial(ctx context.Context, ras addrList) (Conn, error){
   // 执行建立网络连接的逻辑,对每个地址尝试建立连接时,先检查是否超时
   for i, ra := range ras {
       select {
       case <-ctx.Done():
          return nil, &OpError{Op: "dial", Net: sd.network, Source: sd.LocalAddr, Addr: ra, Err: mapErr(ctx.Err())}
       default: 
       }
       // 执行建立网络连接的逻辑
   }
}
复制代码

查看上述代码中,dialSerial函数实现可选超时控制,看起来只是传入参数不同,一个是传入定时器time.Timer实例,一个是传入context.Context接口实例而已,但是实际上不仅仅如此。

首先是代码的可读性上来看,传入time.Timer实例来实现超时控制,并非Go中常见的实现方式,用户不好理解;而对于context.Context接口来说,其被广泛使用,如果要实现超时控制,用户只需要传入一个timerCtx实例即可,用户使用起来没有额外的心智负担,代码可读性更强。

其次是对于整个Go语言的生态来说,context.Context接口在Go语言标准库中得到广泛使用,而且普遍超时控制都是使用timerCtx来实现的,如果此时传入一个time.Timer实例,实际上是与整个Go语言的超时控制的格格不入的。以上面dialSerial方法为例,其建立网络连接是需要调用底层函数来协助实现的,如:

func (fd *netFD) connect(ctx context.Context, la, ra syscall.Sockaddr) (rsa syscall.Sockaddr, ret error) {
    // 执行建立连接的逻辑
    switch err := connectFunc(fd.pfd.Sysfd, ra); err {
    // 未报错,此时检查是否超时
    case nil, syscall.EISCONN:
       select {
       case <-ctx.Done():
           // 如果已经超时,此时返回超时错误
          return nil, mapErr(ctx.Err())
       default:
       }
     }
}
复制代码

而且刚好,该函数也是实现了可选的超时控制,而且是通过timerCtx来实现的,如果此时传入的timerCtx已经超时,此时函数会直接返回一个超时错误。

如果上面dialSerial的超时控制是通过context.Context的接口实例来实现的话,此时调用函数时,直接将外部的Context实例作为参数传入connect函数,外层调用也无需再检查函数是否超时,代码的可复用性更高。

相对的,如果dialSerial的超时控制是通过传入定时器实现的,此时便无法很好利用connect方法已经实现的超时检查的机制。

因此,综上所述,使用 context.Context 接口作为可选的超时控制参数,相比于使用 time.Timer,更加适合同时也更加高效,与整个Go语言的实现也能够更好得进行融合在一起。

4.3 总结

ContextTime 都是 Go 语言中实现超时控制的方法,它们各有优缺点,不能说哪一种实现更好,要根据具体的场景来选择使用哪种方法。

在一些简单的场景下,使用 Time 包实现超时控制可能更加方便,因为它的 API 更加简单,只需要使用 time.After() 函数即可实现超时控制。

但是,如果涉及到在多个函数,或者是需要多个goroutine之间传递的话,此时使用Context来实现超时控制可能更加适合。

5.总结

本文介绍了需要超时控制的原因,主要是避免无限期等待,防止资源泄漏和提高程序响应速度这几点内容。

接着我们介绍了Go语言中实现超时控制的方法,包括使用time实现超时控制以及使用context实现超时控制,并给出了简单的代码示例。

在接下来,我们便这两种实现的适用场景进行分析,明确了在哪些场景下,适合使用time实现超时控制,以及在哪些场景下,使用timerCtx来实现更为高效。

基于此,完成了为什么需要超时控制的介绍,希望能够让大家在遇到需要超时控制的场景下,更好得去进行实现。

Supongo que te gusta

Origin juejin.im/post/7230308610080702520
Recomendado
Clasificación