| // Copyright 2023 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <threads.h> |
| |
| #include <array> |
| #include <atomic> |
| |
| #include "librace.h" |
| #include "model-assert.h" |
| |
| // |
| // Requirements: |
| // |
| // Design a SeqLock which is intended to allow concurrent reads, and exclusive |
| // writes. The sequence number itself serves both as the transaction failure |
| // detector for readers, as well as the spinlock-style exclusive lock for |
| // writers. |
| // |
| // The SeqLock implementation described in this example must tolerate the |
| // following thread behaviors, and obey the following rules. |
| // |
| // ++ Readers are allowed to execute concurrently with writers |
| // ++ Writer may attempt to execute concurrently (from multiple |
| // threads), but the SeqLock implementation must not allow them to |
| // modify the payload at the same time. |
| // ++ Writers *may not* read the payload during the transaction. IOW, |
| // they may only write new values to the payload, which do not |
| // depend on the old values. |
| // ++ Payload AcqRel semantics are to be used in this example, instead |
| // of fences. |
| // |
| // The Hypothesis: |
| // |
| // A minimal implementation of a SeqLock where all writes came from a |
| // single thread would look something like this: |
| // |
| // :: Read Transaction :: |
| // 1) while ((before = Seq) & 0x1) {}; // Acquire semantics |
| // 2) Read Payload; // Acquire semantics for all members |
| // 3) after = Seq; // Relaxed semantics |
| // 4) if (before != after) repeat; |
| // |
| // :: Write Transaction :: |
| // 1) Seq += 1; // Relaxed semantics |
| // 2) Write Payload; // Release semantics for all members |
| // 3) Seq += 1; // Release semantics |
| // |
| // If we wish to extend this to allow writes to come in from multiple |
| // threads, we need to make sure that two writers are not allowed to |
| // modify the payload at the same time. So, we use the "odd sequence |
| // number implies write-in-flight" property of our sequence number to |
| // cause writers to wait in line. The new sequence is now: |
| // |
| // :: Write Transaction :: |
| // 1) while ((observed = Seq) & 0x1) {}; // Relaxed semantics |
| // 2) if (!CMPX(observed, observed +1)) goto 1; // Relaxed semantics |
| // 3) Write Payload; // Release semantics for all members |
| // 4) Seq += 1; // Release semantics |
| // |
| // Note that there are no acquire semantics on either step #1 or step #2. There |
| // are no loads of any of the payload values in step #3, so there is nothing in |
| // step #3 which would be need to be prevented from moving up before steps #1,2. |
| // Therefore, a load-acquire operation at that point in the sequence would serve |
| // no purpose, right? |
| // |
| // Turns out, the answer is "No, this is not fine". |
| // |
| // The Failure: |
| // |
| // The CDS-checker model (below) finds the problem with this idea. We model a |
| // system with one reader (R1), and two writers (W1 and W2), all executing |
| // concurrently. After a successful read, the reader must have observed one of |
| // the following: |
| // |
| // 1) The initial state of the payload. |
| // 2) The state of the payload as written by W1. |
| // 3) The state of the payload as written by W2. |
| // |
| // Any mixing of the payload would be a failure, and indeed, this does fail. |
| // Here is one example from the checker. The addresses of the accesses have |
| // been "symbolized" for ease of reading, and unrelated accesses (those used |
| // when launching threads) have been removed. Finally, the full sequence is |
| // presented in execution order, and then again grouped by thread. |
| // |
| // 1) The initial state of the payload is P = (0, 1) |
| // 2) W1 writes P = (2, 3) |
| // 3) W2 writes P = (4, 5) |
| // |
| // ------------------------------------------------------------------------------------ |
| // # t Action type MO Location Value Rf CV |
| // ------------------------------------------------------------------------------------ |
| // 2 1 atomic write seq_cst Seq 0 ( 0, 2) |
| // 3 1 atomic write seq_cst p[0] 0 ( 0, 3) |
| // 4 1 atomic write seq_cst p[1] 0x1 ( 0, 4) |
| // 7 2 atomic read acquire Seq 0 2 ( 0, 5, 7) |
| // 10 2 atomic read acquire p[0] 0 3 ( 0, 5, 10) |
| // 11 3 atomic read relaxed Seq 0 2 ( 0, 8, 0, 11) |
| // 14 2 atomic read acquire p[1] 0x1 4 ( 0, 5, 14) |
| // 15 3 atomic rmw relaxed Seq 0 2 ( 0, 8, 0, 15) |
| // 16 4 atomic read relaxed Seq 0 2 ( 0, 12, 0, 0, 16) |
| // 17 2 atomic read relaxed Seq 0x1 15 ( 0, 5, 17) |
| // 18 3 atomic write release p[0] 0x2 ( 0, 8, 0, 18) |
| // 19 4 atomic read relaxed Seq 0x1 15 ( 0, 12, 0, 0, 19) |
| // 20 2 atomic read acquire Seq 0x1 15 ( 0, 5, 20) |
| // 21 3 atomic write release p[1] 0x3 ( 0, 8, 0, 21) |
| // 22 4 atomic read relaxed Seq 0x1 15 ( 0, 12, 0, 0, 22) |
| // 24 3 atomic rmw release Seq 0x1 15 ( 0, 8, 0, 24) |
| // 26 2 atomic read acquire Seq 0x2 24 ( 0, 8, 26, 24) |
| // 28 4 atomic read relaxed Seq 0x2 24 ( 0, 12, 0, 0, 28) |
| // 29 2 atomic read acquire p[0] 0x2 18 ( 0, 8, 29, 24) |
| // 30 4 atomic rmw relaxed Seq 0x2 24 ( 0, 12, 0, 0, 30) |
| // 31 2 atomic read acquire p[1] 0x3 21 ( 0, 8, 31, 24) |
| // 32 4 atomic write release p[0] 0x4 ( 0, 12, 0, 0, 32) |
| // 33 2 atomic read relaxed Seq 0x3 30 ( 0, 8, 33, 24) |
| // 34 4 atomic write release p[1] 0x5 ( 0, 12, 0, 0, 34) |
| // 35 2 atomic read acquire Seq 0x3 30 ( 0, 8, 35, 24) |
| // 36 4 atomic rmw release Seq 0x3 30 ( 0, 12, 0, 0, 36) |
| // 39 2 atomic read acquire Seq 0x4 36 ( 0, 12, 39, 24, 36) |
| // 40 2 atomic read acquire p[0] 0x2 18 ( 0, 12, 40, 24, 36) |
| // 41 2 atomic read acquire p[1] 0x5 34 ( 0, 12, 41, 24, 36) |
| // 42 2 atomic read relaxed Seq 0x4 36 ( 0, 12, 42, 24, 36) |
| // |
| // -------------------------- The sequence seen by W1 -------------------------- |
| // |
| // W1 sees no contention at all. It grabs the "lock" and writes (2, 3) to the |
| // payload, then exits. |
| // |
| // 11 3 atomic read relaxed Seq 0 2 ( 0, 8, 0, 11) |
| // 15 3 atomic rmw relaxed Seq 0 2 ( 0, 8, 0, 15) |
| // 18 3 atomic write release p[0] 0x2 ( 0, 8, 0, 18) |
| // 21 3 atomic write release p[1] 0x3 ( 0, 8, 0, 21) |
| // 24 3 atomic rmw release Seq 0x1 15 ( 0, 8, 0, 24) |
| // |
| // -------------------------- The sequence seen by W2 -------------------------- |
| // |
| // W2 starts by reading Seq and seeing an even number. However, we see it immediately |
| // read Seq again (this time observing an odd number), implying that the relaxed |
| // read during the strong CMPX saw a different value from what was expected, and |
| // therefore failed. W2 now spins on Seq, waiting for it to become even again. |
| // |
| // 16 4 atomic read relaxed Seq 0 2 ( 0, 12, 0, 0, 16) |
| // 19 4 atomic read relaxed Seq 0x1 15 ( 0, 12, 0, 0, 19) |
| // 22 4 atomic read relaxed Seq 0x1 15 ( 0, 12, 0, 0, 22) |
| // |
| // We see the sequence number go back to even, and this time we successfully |
| // increment it, write out payload, and drop the lock. |
| // |
| // 28 4 atomic read relaxed Seq 0x2 24 ( 0, 12, 0, 0, 28) |
| // 30 4 atomic rmw relaxed Seq 0x2 24 ( 0, 12, 0, 0, 30) |
| // 32 4 atomic write release p[0] 0x4 ( 0, 12, 0, 0, 32) |
| // 34 4 atomic write release p[1] 0x5 ( 0, 12, 0, 0, 34) |
| // 36 4 atomic rmw release Seq 0x3 30 ( 0, 12, 0, 0, 36) |
| // |
| // -------------------------- The sequence seen by R1 -------------------------- |
| // |
| // R1 starts a read sequence, and manages to observe the initial state of (0,1), |
| // but when it reads the sequence number the second time, it sees that the |
| // counter has been bumped, and it needs to try again. |
| // |
| // 7 2 atomic read acquire Seq 0 2 ( 0, 5, 7) |
| // 10 2 atomic read acquire p[0] 0 3 ( 0, 5, 10) |
| // 14 2 atomic read acquire p[1] 0x1 4 ( 0, 5, 14) |
| // 17 2 atomic read relaxed Seq 0x1 15 ( 0, 5, 17) |
| // |
| // So, R1 goes back to waiting for Seq to become even again. Once it does, R1 |
| // successfully observes (2,3), the state written by W1. Once again, however, |
| // it sees the sequence number advance, and so it has to retry. |
| // |
| // 20 2 atomic read acquire Seq 0x1 15 ( 0, 5, 20) |
| // 26 2 atomic read acquire Seq 0x2 24 ( 0, 8, 26, 24) |
| // 29 2 atomic read acquire p[0] 0x2 18 ( 0, 8, 29, 24) |
| // 31 2 atomic read acquire p[1] 0x3 21 ( 0, 8, 31, 24) |
| // 33 2 atomic read relaxed Seq 0x3 30 ( 0, 8, 33, 24) |
| // |
| // Back to waiting for an even sequence number. When we finally see one, we see |
| // the final sequence number written by W2 (0x4) and proceed to read our |
| // payload. When we get to the end, we see the sequence number is still 0x4, so |
| // we declare victory. |
| // |
| // 35 2 atomic read acquire Seq 0x3 30 ( 0, 8, 35, 24) |
| // 39 2 atomic read acquire Seq 0x4 36 ( 0, 12, 39, 24, 36) |
| // 40 2 atomic read acquire p[0] 0x2 18 ( 0, 12, 40, 24, 36) |
| // 41 2 atomic read acquire p[1] 0x5 34 ( 0, 12, 41, 24, 36) |
| // 42 2 atomic read relaxed Seq 0x4 36 ( 0, 12, 42, 24, 36) |
| // |
| // Unfortunately, the payload we read is (2, 5). We saw p[0] as written by W1, |
| // and p[1] as written by W2. This "should" be impossible given the following |
| // argument: |
| // |
| // 1) The read of Seq at #39 synchronized-with the write of Seq at #36. |
| // 2) The subsequent loads #40-42 must therefore happen-after the write at #36. |
| // 3) The W2 payload stores #32,34 cannot move past the store at #36. |
| // 4) Therefore the payload loads #40,41 must have happened-after the stores |
| // #32,34. |
| // |
| // So how did load #40 manage to see store #18? The clock vector tells the |
| // tale. Load #39 did synchronize-with store #36 in W2, so all of the |
| // subsequent loads did happen after W2 completed. But, it also synchronized |
| // with store #18 from W1, and the loads also happened after W1 completed. |
| // |
| // The key point: |
| // |
| // The issue here is that W1 and W2 have no ordering relationship. While R1 |
| // happened-after both W1 and W2, W1 did not happen-after W2, or vice-versa; |
| // they have no defined order. Therefore, loads done in R1 can see the state |
| // left by W1 after store #24 OR they can see the state left by W2 after store |
| // #36. In this case, that is what happens. p[0] comes from the W1 state, and |
| // p[1] comes from the W2 state. |
| // |
| // The fix: |
| // |
| // The fix here is to add acquire semantics to the initial load of Seq done |
| // during the write sequence. This forces the start of every write transaction |
| // to synchronize with the end of the previous write transaction. Now, all of |
| // the write transactions have a defined order, so when the read sequences |
| // initial Seq load synchronizes-with at the end of a write transaction, it can |
| // only see states after that write transaction, never any earlier states. |
| // |
| |
| constexpr uint32_t kPayloadWords = 2; |
| constexpr uint32_t kTotalWriters = 2; |
| |
| struct State { |
| std::atomic<uint64_t> seq; |
| std::array<std::atomic<uint32_t>, kPayloadWords> payload; |
| }; |
| |
| static void Read(State& s) { |
| uint64_t before, after; |
| std::array<uint32_t, kPayloadWords> vals; |
| |
| do { |
| // spin until we see an even sequence number using load-acquires. |
| while ((before = s.seq.load(std::memory_order_acquire)) & 1u) { |
| thrd_yield(); |
| } |
| |
| // Load all payloads with acquire semantics. |
| for (uint32_t i = 0; i < vals.size(); ++i) { |
| vals[i] = s.payload[i].load(std::memory_order_acquire); |
| } |
| |
| // Load the sequence number again, this time relaxed |
| after = s.seq.load(std::memory_order_relaxed); |
| } while (before != after); |
| |
| MODEL_ASSERT((vals[0] % kPayloadWords) == 0); |
| for (uint32_t i = 1; i < vals.size(); ++i) { |
| MODEL_ASSERT((vals[i - 1] + 1) == vals[i]); |
| } |
| } |
| |
| static void Write(State& s, uint32_t start_val) { |
| // Spin until we can increment the sequence number from even to odd, using |
| // only relaxed semantics. |
| uint64_t seq; |
| do { |
| while ((seq = s.seq.load(std::memory_order_relaxed)) & 1u) { |
| thrd_yield(); |
| } |
| } while (!s.seq.compare_exchange_strong(seq, seq + 1, std::memory_order_relaxed)); |
| |
| // Store the payloads, each with release semantics. |
| for (size_t i = 0; i < s.payload.size(); ++i) { |
| s.payload[i].store(start_val + i, std::memory_order_release); |
| } |
| |
| // Increment the sequence number again, this time with release. |
| s.seq.fetch_add(1u, std::memory_order_release); |
| } |
| |
| extern "C" int user_main(int argc, char** argv) { |
| State s; |
| |
| printf("Seq = %p\n", &s.seq); |
| s.seq.store(0u); |
| for (uint32_t i = 0; i < s.payload.size(); ++i) { |
| printf("p[%i] = %p\n", i, &s.payload[i]); |
| s.payload[i].store(i); |
| } |
| |
| thrd_t reader; |
| struct WriterState { |
| State* state; |
| thrd_t thread; |
| uint32_t id; |
| }; |
| |
| std::array<WriterState, kTotalWriters> writers; |
| for (uint32_t i = 0; i < writers.size(); ++i) { |
| writers[i].state = &s; |
| writers[i].id = i + 1; |
| } |
| |
| thrd_create( |
| &reader, |
| [](void* ctx) { |
| State& s = *reinterpret_cast<State*>(ctx); |
| Read(s); |
| }, |
| &s); |
| |
| for (auto& writer : writers) { |
| thrd_create( |
| &writer.thread, |
| [](void* ctx) { |
| WriterState& ws = *reinterpret_cast<WriterState*>(ctx); |
| Write(*ws.state, kPayloadWords * ws.id); |
| }, |
| &writer); |
| } |
| |
| thrd_join(reader); |
| for (auto& writer : writers) { |
| thrd_join(writer.thread); |
| } |
| |
| return 0; |
| } |