sync example note2:memory order
coconutnut

Producer consumer example

There might be conflict on buffer[], but consumed_until and produced_until prevent that (if used correctly).

So, only need to make sure consumed_until and produced_until accessed correctly under concurrency.

Using locks

In each round of produce()

1
2
3
4
5
6
7
8
9
10
while (true) {
lock_acquire(&lock);
if (consumed_until + BUFFER_SIZE > r) break;
lock_release(&lock);
}

produced[r] = ...
buffer[r % BUFFER_SIZE] = produced[r];
produced_until++;
lock_release(&lock);

Two locks:

  1. check if can produce: conflict on consumed_until
  2. conflict on produced_until

In each round of consume()

1
2
3
4
5
6
7
8
9
while (true) {
lock_acquire(&lock);
if (produced_until > r) break;
lock_release(&lock);
}

consumed[r] = buffer[r % BUFFER_SIZE];
consumed_until++;
lock_release(&lock);

Two locks:

  1. check if can consume: conflict on produced_until
  2. conflict on consumed_until

Using atomic vairables

In produce, two locks can be modified:

  1. check if can produce: conflict on consumed_until

    no ordering constraints -> memory_order_relaxed

  2. conflict on produced_until

    store produced_until should happen after all operations -> load-store + store-store = release

1
2
3
4
5
6
7
8
9
10
11
12
while (true) {
// The memory order can be relaxed as we don't read anything "produced" by the consumer.
int local_cu = atomic_load_explicit(&consumed_until, memory_order_relaxed);
if (local_cu + BUFFER_SIZE > r) break;
}

produced[r] = ...
buffer[r % BUFFER_SIZE] = produced[r];
// We want to increment "produced_until" after the buffer has been written.
// By using memory_order_release, we prevent the STOREs on buffer from being
// reordered after the atomic operation.
atomic_fetch_add_explicit(&produced_until, 1, memory_order_release);

In consume, two locks can be modified:

  1. check if can consume: conflict on produced_until

    load produced_until should happen before all operations -> load-load + load-store = acquire

  2. conflict on consumed_until

    store consumed_until should happen after all operations -> load-store + store-store = release

1
2
3
4
5
6
7
8
9
while (true) {
// We don't want to access the buffer before checking the atomic variable.
// The memory_order_acquire prevents this reordering.
int local_pu = atomic_load_explicit(&produced_until, memory_order_acquire);
if (local_pu > r) break;
}

consumed[r] = buffer[r % BUFFER_SIZE];
atomic_fetch_add_explicit(&consumed_until, 1, memory_order_release);

Memory barriers

std::memory_order

Fences are Memory Barriers

Memory Barriers - Learn Modern C++

  • memory_order_relaxed: no ordering constraints

  • memory_order_consume

  • memory_order_acquire: load-load + load-store

  • memory_order_release: load-store + store-store

  • memory_order_acq_rel

  • memory_order_seq_cst