仅使用原子实现以下代码:
localhost:8080
如下:
const Max = 8
var index int
func add() int {
index++
if index >= Max {
index = 0
}
return index
}
但这是错误的。有竞争条件。 可以实现不使用锁?
答案 0 :(得分:3)
一个简单的实现可能如下所示:
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
,&#34;我们已经完成&#34;,我们可以退货。
如果它大于或等于Max
,那么我们必须对值进行标准化(确保它在[0..Max)
范围内)并重置计数器。< / p>
重置可能只能由一个调用者(一个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
}
让我们来分析一个极端的&#34;场景。在此,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&#34;等待&#34;重置操作。在实践中,只有很小的机会发生这种情况,但是这个解决方案可以很好地有效地处理它。
答案 1 :(得分:1)
我认为您的目标是永远不要让index
的值等于或大于Max
。这可以使用CAS(比较和交换)循环来解决:
const Max = 8
var index int32
func add() int32 {
var next int32;
for {
prev := atomic.LoadInt32(&index)
next = prev + 1;
if next >= Max {
next = 0
}
if (atomic.CompareAndSwapInt32(&index, prev, next)) {
break;
}
}
return next
}
CAS可用于像这样原子地实现几乎任何操作。算法是: