【问题标题】:Why when I timeout a function a defer is not called?为什么当我超时一个函数时不调用延迟?
【发布时间】:2018-11-06 06:39:05
【问题描述】:

当我在函数中添加一个 defer 时,我希望它总是在函数结束时被调用。 我注意到当函数超时时它不会发生。

package main

import (
    "context"
    "fmt"
    "time"
)

func service1(ctx context.Context, r *Registry) {
    ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
    defer func() {
        r.Unset("service 1")
    }()
    r.Set("service 1")
    go service2(ctx, r)

    select {
    case <-ctx.Done():
        cancel()
        break
    }
 }

 func service2(ctx context.Context, r *Registry) {
    defer func() {
        r.Unset("service 2")
    }()

    r.Set("service 2")

    time.Sleep(time.Millisecond * 300)
 }

         type Registry struct {
    entries map[string]bool
 }

 func (r *Registry)Set(key string) {
    r.entries[key] = true
 }

 func (r *Registry)Unset(key string)  {
    r.entries[key] = false
 }

 func (r *Registry)Print() {
    for key, val := range r.entries  {
        fmt.Printf("%s -> %v\n", key, val)
    }
 }

 func NewRegistry() *Registry {
    r := Registry{}
    r.entries = make(map[string]bool)

    return &r
 }

func main() {
    r := NewRegistry()

    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*200)

    go service1(ctx, r)
    // go service3(ctx, r)

    select {
    case <-ctx.Done():
        fmt.Printf("context err: %s\n", ctx.Err())
        cancel()
    }

    r.Print()
 }

在上面的示例中,service2() 中的 defer 从未被调用,这就是输出为:

service 1 -> false
service 2 -> true

而不是

service 1 -> false
service 2 -> false

我知道超时意味着“停止执行”,但执行延迟代码对我来说是合理的。我找不到对这种行为的任何解释。

以及问题的第二部分——如何修改服务或Registry 来抵抗这种情况?

【问题讨论】:

  • 可能是竞争条件?您的 r 注册表变量没有锁定?
  • main 函数在service2 返回之前调用r.Print()。地图上有数据竞赛(使用go -race main.go 运行程序以查看竞赛)。
  • 你的程序可以打印任何东西,例如甚至是service 1 -&gt; hubbu bubba trallala,因为它是畸形的,因为它是活泼的。
  • “从不调用 service2() 中的延迟”。是的。 service2 开始运行后大约 300 毫秒。 “我明白超时意味着‘停止执行’”。不,它没有,至少没有关于上下文包。取消上下文并没有做任何花哨的事情。它只是关闭了一个频道。
  • 请在问题中输入您的代码。 Playgrounds 链接很棒,但不应要求您理解您的问题 - 特别是考虑到外部链接最终往往会过时。

标签: go concurrency


【解决方案1】:

第一部分的答案

假设您有一个函数f1(),它使用defer 调用f2(),即defer f2()。事实上,当且仅当f1 完成时,即使发生运行时恐慌,f2 才会被调用。更具体地说,请查看go-defer

现在我们关心的是在 goroutine 中使用 defer。我们还必须记住,如果它的父函数完成了退出,则 go-routine 将退出。

所以如果我们在 go-routine 函数中使用defer,那么如果父函数完成或退出,那么 go-routine 函数必须退出。由于它退出(未完成),defer 语句将不会执行。很明显,我们绘制了您的程序的状态。 如你所见,

  • 在第 1 毫秒,service1() 在其他时间之前完成。因此,service2() 退出而不执行 defer 语句,并且“服务 2”不会设置为 false。由于service1() 完成,它的defer 将执行并且'service 1' 将被设置为false
  • 在第 2 毫秒,main() 完成并且程序完成。

所以我们看看这个程序是如何执行的。

第二部分的答案

我尝试的一种可能的解决方案是增加service1() 的时间或减少service2() 的时间。

【讨论】:

  • 我添加了超时来模拟service2()执行时间比service1()长的情况,以学习如何处理这种情况。所以第 2 部分的答案不是我的问题的答案。但是感谢您的解释!对我帮助很大。
  • 您的意思是service2() 的超时时间小于service1() 的超时时间吗?
  • 不完全...我想模拟service1() 正在运行并执行 defer 的情况。但是,service2() 仍在运行,然后超时。我正在考虑将缓冲通道作为您解释的数据条件的解决方案。
  • 在这种情况下,您是从service1() 致电service2() 吗?我/我只是想知道。
  • 正确!看起来是这样的 -> main() -> go service1() ->go service2()
猜你喜欢
  • 1970-01-01
  • 2016-03-22
  • 1970-01-01
  • 1970-01-01
  • 2020-07-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-30
相关资源
最近更新 更多