确保仅在

时间:2018-05-04 19:35:22

标签: go concurrency synchronisation

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

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

我记得以前见过这样的东西,但即使我尝试过,我现在也找不到它。搜索具有值和类似术语的sync.Once并未产生任何有用的结果。我知道我可以使用互斥锁和大量锁定来实现这一点,但是互斥锁和大量锁定似乎不是推荐的方法。

1 个答案:

答案 0 :(得分:2)

一般“初始化”解决方案

在一般/通常情况下,只有init一次的最简单的解决方案,只有在实际需要时才使用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()) } 。该函数中的全局变量。

见这个简单的例子:

Fetching total...
1
1

上述输出(在Go Playground上尝试):

sync.Once

一些注意事项:

  • 您可以使用互斥锁或GetTotal()来实现相同功能,但后者实际上比使用互斥锁更快。
  • 如果之前已调用过GetTotal(),则对Once.Do()的后续调用将不会执行任何操作,只会返回先前计算的值,这是sync.Once执行/确保的操作。 Do()“跟踪”之前是否已调用其sync.Once方法,如果是,则不再调用传递的函数值。
  • total提供了此解决方案的所有需求,可以安全地同时使用多个goroutine,因为您不会直接从其他任何位置修改或访问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()变量} field。

如果首先调用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 } 函数:

claimRef