Launch two threads. Both run the same one line, increment a counter. The counter starts at zero. What is the final value? It feels obvious that it is two, one for each thread. But it might be one. Two increments went in and only one came out, and nothing crashed, nothing warned you. Your program is just quietly wrong. This is a race condition, where correctness depends on the unpredictable timing of who runs when. Understanding it is the difference between a kernel you can trust and a kernel that lies to you on some runs and not others.
Start with why incrementing a counter is dangerous, because it looks harmless. The trap is that increment is not one operation. It is three. First the thread reads the counter's current value out of memory into a private register. Then it adds one to that register. Then it writes the register back to memory. Read, modify, write. Three separate steps with real gaps between them, and those gaps are where the disaster lives.
Walk two threads through it in the unlucky order. Thread A reads the counter and sees zero. Before thread A writes anything back, thread B also reads the counter, and it also sees zero, because A hasn't written yet. Now both threads hold zero in their private registers. Both add one, both compute one. Thread A writes one back. Then thread B writes one back, right on top of A's result.
↳ The counter ends at one. Thread B never saw A's update, because it read the value before A's write had landed. One of the two increments simply evaporated. Scale this to ten thousand threads and you don't get ten thousand, you get some smaller, random number that changes every run.
Now the natural question. Is this just a bug in my code, or is the hardware actually working against me? It is the hardware, by design, for three concrete reasons.
So how do you fix it? And here is the most important and most misunderstood point in all of this. The barrier you use to make threads wait for each other does not fix this race.
It is tempting to think, just make everyone pause between reading and writing. But watch it fail. Every thread reads the counter before the pause, so they all see the same value. Then they all wait at the pause. Then they all write their incremented value, and the last one to write still stomps on everyone else.
↳ The pause synchronized the phases, everyone read then everyone wrote, but it did nothing to protect the single update from being overwritten. A barrier orders when things happen, it does not make the three-step increment indivisible.
What actually fixes it is an atomic operation. An atomic increment performs the read, the modify, and the write as one single indivisible hardware transaction.
While one thread is doing it, the hardware locks that memory location so no other thread can read or write it until the whole read-modify-write is complete. The threads are forced to take turns on that one spot. So ten thousand atomic increments correctly produce ten thousand, because none of them can slip into the gap of another, there is no gap.
↳ The cost is that it is slower than a plain increment, because it serializes access. Threads line up for that location instead of charging in parallel. But slow and correct beats fast and wrong.
It is worth being precise about when a barrier is the right tool and when it is not, because both exist for real reasons. A barrier is perfect for a single-writer, many-reader pattern. One thread computes a value and writes it, everyone hits the barrier so that write is guaranteed done and visible, then everyone safely reads it. No race, because only one thread ever writes. The barrier protects readers from seeing a half-finished write.
Atomics are for the opposite situation, many threads all updating the same location, where you need each update to survive. Use the barrier to separate a writing phase from a reading phase. Use atomics when many writers hit one spot. They solve different problems and are not interchangeable.
There is also a finer-grained cousin of the block barrier worth knowing. The full barrier makes every thread in the whole block wait, and it lives in the block's shared memory hardware. But sometimes you only need the 32 threads of a single warp to agree, and for that there is a lighter warp-level sync.
And this raises a fair question. Don't the 32 threads of a warp already run in perfect lockstep, so why sync them at all? They used to, but on modern hardware that is no longer guaranteed.
Newer architectures let threads within the same warp drift to different points in the program and reconverge, so you can no longer assume that if one thread in a warp wrote something, its neighbor has already seen it. The warp-level sync makes the 32 threads line up and agree on memory, and it is cheaper than the full block barrier because it only involves one warp's worth of hardware, not the whole block. If all you need is agreement inside a warp, reach for the lighter one.
So the whole picture. A race condition is when your answer depends on the accidental timing of threads, and it strikes hardest on read-modify-write, because that trio of steps has gaps that another thread can invade, leaving updates to vanish silently.
The hardware makes this the default through unpredictable scheduling, multi-step updates, and delayed propagation. A barrier does not save you here, it only orders phases, it cannot make an increment indivisible. Atomics are the real fix for many-writer updates, locking the location so each update survives at the cost of serializing.
Shared memory on a GPU is a commons with no automatic protection.
The hardware will not stop you from writing wrong code, it will just quietly give you a different wrong answer every time.