Skip to content

Commit d7825cf

Browse files
Rudolf Rakosbrian-brazil
authored andcommitted
Use atomics instead of synchronized for Gauge (#482)
* Use atomic update instead of synchronized for Gauge * Introduce DoubleAdder#set to avoid allocations in Gauge#set Signed-off-by: Rudolf Rakos <[email protected]>
1 parent 10fa80b commit d7825cf

File tree

2 files changed

+69
-12
lines changed

2 files changed

+69
-12
lines changed

simpleclient/src/main/java/io/prometheus/client/DoubleAdder.java

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,37 @@ public void add(double x) {
9494
* @return the sum
9595
*/
9696
public double sum() {
97-
Cell[] as = cells;
98-
double sum = Double.longBitsToDouble(base);
97+
// On concurrent `sum` and `set`, it is acceptable to `get` an outdated `value`.
98+
// On concurrent `sum` and `add`, it is acceptable to `get` an outdated `value`.
99+
// On concurrent `sum` and `set` and `add`, it is possible to `get` an outdated `value`.
100+
101+
// Correctness is guaranteed by `volatile` memory access ordering and visibility semantics.
102+
// Program order:
103+
// - writes in `set` - `busy` (CAS), `cells` (Wc), `base` (Wb), `busy`
104+
// - reads in `sum` - `cells` (Rc), `base` (Rb), `busy`, `cells` (Cc), `base` (Cb)
105+
// Note that:
106+
// - `busy` is written after `cells` and `base`
107+
// - `busy` is read after `cells` and `base`, then `cells` and `base` is re-read after `busy`
108+
// In other words:
109+
// - if we see the write to `busy`, then we must see the write to `cells` and `busy` on re-read
110+
// - if we don't see the write to `busy`, then we must retry as we have no guarantees
111+
// Execution order (in the former case):
112+
// - serial
113+
// - old result - Rc, Rb, Cc, Cb, Wc, Wb
114+
// - new result - Wc, Wb, Rc, Rb, Cc, Cb
115+
// - concurrent
116+
// - old result - Rc, Wc, Rb, Wb, Cc, Cb - retry (superfluous)
117+
// - new result - Wc, Rc, Wb, Rb, Cc, Cb
118+
// - invalid result - Rc, Wc, Wb, Rb, Cc, Cb - retry
119+
// - invalid result - Wc, Rc, Rb, Wb, Cc, Cb - retry
120+
Cell[] as = cells; long b = base;
121+
while (as != null && !(busy == 0 && cells == as && base == b)) {
122+
// busy waiting, retry loop
123+
Thread.yield();
124+
as = cells; b = base;
125+
}
126+
127+
double sum = Double.longBitsToDouble(b);
99128
if (as != null) {
100129
int n = as.length;
101130
for (int i = 0; i < n; ++i) {
@@ -118,6 +147,41 @@ public void reset() {
118147
internalReset(0L);
119148
}
120149

150+
public void set(double x) {
151+
// On concurrent `set` and `set`, it should be acceptable to lose one `set` measurement.
152+
// On concurrent `set` and `add`, it should be acceptable to lose the `add` measurement.
153+
154+
// Correctness is ensured by different techniques:
155+
// - `set` waits on contention (blocking)
156+
// - `add` avoids contention (non-blocking)
157+
// - `sum` retries on conflicts (non-blocking)
158+
// Performance characteristics by use cases:
159+
// - only `set` - `cells` is always `null` - no allocations
160+
// - only `add` - `cells` allocated on contention
161+
// - mixed `set` and `add` - `cells` allocated on contention, `cells` deallocated on `set`
162+
for (;;) {
163+
Cell[] as;
164+
if ((as = cells) != null) { // have cells
165+
if (busy == 0 && casBusy()) {
166+
try {
167+
if (cells == as) { // recheck under lock
168+
// update cells and base (not atomic)
169+
cells = null;
170+
base = Double.doubleToLongBits(x);
171+
break;
172+
}
173+
} finally {
174+
busy = 0;
175+
}
176+
}
177+
} else { // no cells
178+
// update base (atomic)
179+
base = Double.doubleToLongBits(x);
180+
break;
181+
}
182+
}
183+
}
184+
121185
/**
122186
* Equivalent in effect to {@link #sum} followed by {@link
123187
* #reset}. This method may apply for example during quiescent

simpleclient/src/main/java/io/prometheus/client/Gauge.java

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ public void close() {
134134
* {@link SimpleCollector#remove} or {@link SimpleCollector#clear},
135135
*/
136136
public static class Child {
137+
137138
private final DoubleAdder value = new DoubleAdder();
138139

139140
static TimeProvider timeProvider = new TimeProvider();
@@ -166,13 +167,7 @@ public void dec(double amt) {
166167
* Set the gauge to the given value.
167168
*/
168169
public void set(double val) {
169-
synchronized(this) {
170-
value.reset();
171-
// If get() were called here it'd see an invalid value, so use a lock.
172-
// inc()/dec() don't need locks, as all the possible outcomes
173-
// are still possible if set() were atomic so no new races are introduced.
174-
value.add(val);
175-
}
170+
value.set(val);
176171
}
177172
/**
178173
* Set the gauge to the current unixtime.
@@ -234,9 +229,7 @@ public <E> E setToTime(Callable<E> timeable){
234229
* Get the value of the gauge.
235230
*/
236231
public double get() {
237-
synchronized(this) {
238-
return value.sum();
239-
}
232+
return value.sum();
240233
}
241234
}
242235

0 commit comments

Comments
 (0)