Optimistic concurrency control is one of the tools ATS uses to create a high performance logging subsystem. For various reasons, mutexes were deemed to be too expensive to be used in the logging fast path. Instead, ATS uses an optimistic commit/rollback strategy to synchronize shared memory. The most typical use of the commit/rollback strategy in the logging subsystem is when the current LogBuffer is full and we need to allocate a new LogBuffer. Since multiple threads can be writing to the current LogBuffer at any given time, ATS needs a way to synchronize access to the current LogBuffer.
The pointer to the current LogBuffer is defined here:
head_p
is defined in
lib/ts/ink_queue.h:
#if (defined(__i386__) || defined(__arm__) || defined(__mips__)) && (SIZEOF_VOIDP == 4)
typedef int32_t version_type;
typedef int64_t data_type;
#elif TS_HAS_128BIT_CAS
typedef int64_t version_type;
typedef __int128_t data_type;
#else
typedef int64_t version_type;
typedef int64_t data_type;
#endif
struct {
void *pointer;
version_type version;
} s;
data_type data;
} head_p;
where s.pointer
is a pointer we want to serialize access
for and s.version
is a counter to tell when
head_p
has been modified. The version, at least in the
context of a LogBuffer, lets us know how many threads are currently
holding a reference to s.pointer
.
But why a union? The key insight here is that data_type
and struct s
are the same size. This means that we can do
an atomic CAS on
s
by simply referring to head_p.data
. This
lets us avoid complicated bit fiddling while still being able to still
do accesses like head_p.s.pointer
. But wait, isn’t this
undefined? As it turns out, according to the C++ spec, it
is in fact
“undefined behavior to read from the member of the union that wasn’t most recently written.”
However, that sentence is quickly followed by
“Many compilers implement, as a non-standard language extension, the ability to read inactive members of a union.”
ATS is relying on non-standard language extensions, whoopee. That being said, ATS has been in use for the better part of two decades, so if I were you I wouldn’t start losing sleep over this just yet.
When we actually want to change the values held in
head_p
, we obey this pattern:
do {
INK_QUEUE_LD(old_h, m_log_buffer);
if (FREELIST_POINTER(old_h) != FREELIST_POINTER(h)) {
ink_atomic_increment(&buffer->m_references, -1);
// another thread should be taking care of creating a new
// buffer, so delete new_buffer and try again
delete new_buffer;
break;
}
} while (!write_pointer_version(&m_log_buffer, old_h, new_buffer, 0));
There’s a lot of macro magic going on here. To spare you the details, here’s a quick summary of what each macro does:
INK_QUEUE_LD(x, y)
: Atomic copy-by-value of
y
into x
.
FREELIST_POINTER(x)
: Maps to
x.s.pointer
.
ink_atomic_increment(x, y)
: Atomically increments
x
by y
.
write_pointer_version(a, b, c, d)
: Atomic CAS between
a
and b
with the new value being a
head_p
with s.pointer = c
and
s.version = d
.
The entire do-while loop of goodness guarantees that anything
executed inside of the loop body is done so atomically. This is
opportunistic because if another thread comes along and changes
m_log_buffer
right after we call
INK_QUEUE_LD()
, the CAS inside
write_pointer_version(..)
will catch the change and abort
the write. The loop repeats until we succeed in atomically performing
the actions inside the loop body.
At first this may seem like a better, more lightweight solution over locks, but it does come with certain drawbacks:
If the critical section is highly contested, then performance quickly degrades. Every failed transaction generates more work, and more work generates more failed transactions. On the other hand, mutexes will put the thread to sleep at the added cost of a context switch.
It is easy to create a time to check to time of use bug with this method. If we forget to wrap the expression in a do-while with the correct terminating condition, we expose ourselves to a TOCTTOU bug. As with the C++ language itself, this form of concurrency control gives the programmer a lot of power at the expense of naive safety.