【问题标题】:Ensuring a value is retrieved only once确保仅检索一次值
【发布时间】:2018-05-04 19:35:21
【问题描述】:

我正在开发一个 Go 包来访问 Web 服务(通过 HTTP)。每次我从该服务检索数据页面时,我都会获得可用页面的总数。获得此总数的唯一方法是获取其中一页(通常是第一页)。但是,对该服务的请求需要时间,我需要执行以下操作:

当在Client 上调用GetPage 方法并且第一次检索页面时,检索到的总数应该存储在该客户端的某个位置。当调用Total 方法并且尚未检索到总数时,应获取第一页并返回总数。如果之前通过调用GetPageTotal 检索到总数,则应该立即返回,根本不需要任何HTTP 请求。这需要多个 goroutine 安全使用。我的想法类似于sync.Once,但传递给Do 的函数返回一个值,然后在调用Do 时自动返回该值。

我记得以前看到过类似的东西,但即使我试过了,现在也找不到。使用价值和类似术语搜索 sync.Once 并没有产生任何有用的结果。我知道我可能可以使用互斥锁和大量锁定来做到这一点,但互斥锁和大量锁定似乎不是在 go 中做事的推荐方式。

【问题讨论】:

  • 如果你不认为互斥锁是 Go 中做事的方式,你应该have a look at the source of sync.Once
  • sync.Once 是您通常确保某事只执行一次的方式。您实际使用时遇到了什么问题?

标签: go concurrency synchronisation


【解决方案1】:

一般的“init-once”解决方案

在一般/通常情况下,仅在实际需要时仅初始化一次的最简单解决方案是使用sync.Once 及其Once.Do() 方法。

您实际上不需要从传递给Once.Do() 的函数返回任何值,因为您可以将值存储到例如该函数中的全局变量。

看这个简单的例子:

var (
    total         int
    calcTotalOnce sync.Once
)

func GetTotal() int {
    // Init / calc total once:
    calcTotalOnce.Do(func() {
        fmt.Println("Fetching total...")
        // Do some heavy work, make HTTP calls, whatever you want:
        total++ // This will set total to 1 (once and for all)
    })

    // Here you can safely use total:
    return total
}

func main() {
    fmt.Println(GetTotal())
    fmt.Println(GetTotal())
}

上面的输出(在Go Playground上试试):

Fetching total...
1
1

一些注意事项:

  • 您可以使用互斥锁或sync.Once 实现相同的效果,但后者实际上比使用互斥锁更快。
  • 如果之前已经调用过GetTotal(),那么随后对GetTotal() 的调用将不会执行任何操作,只会返回先前计算的值,这是Once.Do() 所做/确保的。 sync.Once "tracks" 如果它的 Do() 方法之前被调用过,如果是这样,传递的函数值将不再被调用。
  • sync.Once 提供了该解决方案的所有需求,可以安全地从多个 goroutine 并发使用,前提是您不会直接从其他任何地方修改或访问 total 变量。

“不寻常”案例的解决方案

一般情况假设total 只能通过GetTotal() 函数访问。

在您的情况下,这不成立:您想通过GetTotal() 函数访问它并且您想在GetPage() 调用之后设置它(如果尚未设置) .

我们也可以用sync.Once 解决这个问题。我们需要上面的GetTotal() 函数;并且当执行GetPage() 调用时,它可能会使用相同的calcTotalOnce 来尝试从接收到的页面设置其值。

它可能看起来像这样:

var (
    total         int
    calcTotalOnce sync.Once
)

func GetTotal() int {
    calcTotalOnce.Do(func() {
        // total is not yet initialized: get page and store total number
        page := getPageImpl()
        total = page.Total
    })

    // Here you can safely use total:
    return total
}

type Page struct {
    Total int
}

func GetPage() *Page {
    page := getPageImpl()

    calcTotalOnce.Do(func() {
        // total is not yet initialized, store the value we have:
        total = page.Total
    })

    return page
}

func getPageImpl() *Page {
    // Do HTTP call or whatever
    page := &Page{}
    // Set page.Total from the response body
    return page
}

这是如何工作的?我们在变量calcTotalOnce 中创建并使用单个sync.Once。这确保了它的Do() 方法只能调用传递给它的函数一次,无论在哪里/如何调用这个Do() 方法。

如果有人首先调用GetTotal() 函数,那么它内部的函数字面量将运行,它调用getPageImpl() 来获取页面并从Page.Total 字段初始化total 变量。

如果首先调用GetPage() 函数,那么它也会调用calcTotalOnce.Do(),它只是将Page.Total 值设置为total 变量。

无论哪条路线先走,这将改变calcTotalOnce 的内部状态,它会记住total 计算已经运行,并且进一步调用calcTotalOnce.Do() 将永远不会调用传递给它的函数值。

或者只是使用“渴望”初始化

另请注意,如果可能必须在程序的生命周期内获取此总数,则可能不值得上述复杂性,因为您可以在创建变量时轻松初始化一次。

var Total = getPageImpl().Total

或者如果初始化有点复杂(例如需要错误处理),使用包init()函数:

var Total int

func init() {
    page := getPageImpl()
    // Other logic, e.g. error handling
    Total = page.Total
}

【讨论】:

  • 那么如何判断该值是否已经被获取呢?我知道我可以使用 bool,但这安全吗?如果一个 goroutine 从该 bool 中读取,而另一个 goroutine 写入它会怎样?
  • 你不需要检查。该检查由Once.Do() 完成。如果你的函数被调用,它还没有被设置。
  • total 可能已经由较早的 GetPage 调用设置,在这种情况下 GetTotal 不应该做任何事情。我不认为 sync.Once 是这里的解决方案。只需使用互斥锁。
  • @Peter 互斥锁比使用sync.Once 慢。结果是一样的。
  • 我现在才意识到可以将不同的函数传递给 Do。我一直认为您必须在每个呼叫站点传递相同的函数。现在明白了,谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-11
相关资源
最近更新 更多