在没有循环和锁的情况下解决它
一个简单的实现可能如下所示:
const Max = 8
var index int64
func Inc() int64 {
value := atomic.AddInt64(&index, 1)
if value < Max {
return value // We're done
}
// Must normalize, optionally reset:
value %= Max
if value == 0 {
atomic.AddInt64(&index, -Max) // Reset
}
return value
}
它是如何工作的?
它只是将计数器加 1; atomic.AddInt64() 返回新值。如果小于Max,“我们完成了”,我们可以返回它。
如果它大于或等于Max,那么我们必须标准化这个值(确保它在[0..Max)的范围内)并重置计数器。
重置只能由单个调用者(单个 goroutine)完成,它将由计数器的值选择。获胜者将是使计数器到达Max的人。
避免需要锁的技巧是通过添加-Max 来重置它,而不是通过将其设置为0。由于计数器的值是标准化的,所以如果其他 goroutine 调用它并同时递增它,它不会造成任何问题。
当然,有许多 goroutine 同时调用这个 Inc(),在应该重置它的 goroutine 实际执行重置之前,计数器可能会增加更多 Max 倍,这将导致计数器达到或超过2 * Max 甚至3 * Max(一般为:n * Max)。因此,我们通过使用value % Max == 0 条件来决定是否应该进行重置来处理这个问题,对于n 的每个可能值,这只会在单个goroutine 上发生。
简化
请注意,标准化不会更改 [0..Max) 范围内的值,因此您可以选择始终执行标准化。如果你愿意,你可以把它简化成这样:
func Inc() int64 {
value := atomic.AddInt64(&index, 1) % Max
if value == 0 {
atomic.AddInt64(&index, -Max) // Reset
}
return value
}
读取计数器而不增加它
不应直接访问index 变量。如果需要读取计数器的当前值而不增加它,可以使用以下函数:
func Get() int64 {
return atomic.LoadInt64(&index) % Max
}
极端情况
让我们分析一个“极端”的场景。在此,Inc() 被调用 7 次,返回数字 1..7。现在在增量之后对Inc() 的下一次调用将看到计数器位于8 = Max。然后它将值标准化为0 并希望重置计数器。现在假设在实际执行重置(即添加-8)之前,发生了8 个其他调用。他们将计数器递增 8 次,最后一次将再次看到计数器的值是 16 = 2 * Max。所有调用都会将值标准化到0..7 范围内,最后一次调用将再次继续执行重置。假设此重置再次延迟(例如出于调度原因),还有另外 8 个调用进入。最后,计数器的值将是 24 = 3 * Max,最后一次调用将继续执行重置。
请注意,所有调用都只会返回[0..Max) 范围内的值。一旦执行了所有重置操作,计数器的值将正确地为0,因为它的值是24,并且有3个“挂起”的重置操作。在实践中,这种情况发生的可能性很小,但这个解决方案可以很好地有效地处理它。