go - 确保值仅被检索一次

标签 go concurrency synchronization

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

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

我记得以前见过类似的东西,但现在即使我尝试了也找不到它。正在寻找 sync.Once具有值(value)和类似的术语没有产生任何有用的结果。我知道我可能可以使用互斥锁和大量锁定来做到这一点,但是互斥锁和大量锁定似乎并不是在 go 中执行操作的推荐方法。

最佳答案

通用“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 “跟踪”如果是 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
}

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

如果有人调用GetTotal()首先运行函数,然后运行其中的函数文字,调用 getPageImpl()获取页面并初始化 total来自 Page.Total 的变量字段。

如果GetPage()函数将首先被调用,它也会调用 calcTotalOnce.Do()它只是设置 Page.Totaltotal变量。

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

或者只是使用“eager”初始化

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

var Total = getPageImpl().Total

或者,如果初始化稍微复杂一点(例如需要错误处理),请使用包 init()功能:

var Total int

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

关于go - 确保值仅被检索一次,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50182009/

相关文章:

java - 我应该何时以及如何使用 ThreadLocal 变量?

python - 如何通过python同步两个ftp服务器

go - 使用 goroutine 处理退避

java - 多线程最佳实践: constraining tasks newFixedThreadPool

java - ExecutorService 固定池线程挂起

c# - .NET 的 ManualResetEvent 和 WaitHandle 的 Java 等价物

api - 在内部同步内部或外部通知监听器

c++ - 指针类型在结构内的对象类型上的用法是什么?

http - 在 golang 中使用示例正文字符串创建 http.Response 实例

c - 在 Golang 中释放 C 变量?