SystemVerilog Scheduling Semantics established that events within the Active region can be processed in any order — the simulator is free to choose. Most of the time this doesn't matter. But when two separate processes both read and write the same variable at the same trigger, that freedom becomes a real problem: the exact same source code can legitimately produce different results depending purely on an order the language never promises to fix.

12 min read | Intermediate Level

What You'll Learn

  • What makes something a genuine race condition, rather than just confusing timing
  • Why two blocking assignments to the same variable, from different always blocks triggered by the same event, produce genuinely unpredictable results
  • Why the exact same scenario becomes fully deterministic once nonblocking assignments are used instead
  • The standard, widely-used guidelines for avoiding races in both design and testbench code

What Makes Something a Race Condition?

A race condition exists when a simulation's outcome depends on the relative order two or more events execute in, and that order isn't something the language actually guarantees. It isn't the same as a bug that always misbehaves the same way — a race can pass for months of regressions and then produce a different result the moment the simulator, the seed, or even just the surrounding code changes enough to shift scheduling order.

A Genuine Race: Two Blocking Assignments, Same Trigger

Here, two separate always blocks both trigger on the same clock edge. One writes a; the other reads a to compute b:


module tb;
  reg clk;
  reg [7:0] a, b;

  always #10 clk = ~clk;

  // Block 1
  always @ (posedge clk) begin
    a = 5;
  end

  // Block 2
  always @ (posedge clk) begin
    b = a + 1;
  end
endmodule

Both blocks execute in the Active region, and the Active region processes its events in whatever order the simulator chooses — the language doesn't specify which of these two blocks runs first. Both outcomes below are equally valid:

If Block 1 runs firstIf Block 2 runs first
a becomes 5 immediately, then Block 2 reads the new a → b = 6Block 2 reads whatever a was before this edge, then Block 1 sets a = 5 → b is something else entirely

Neither result is wrong, according to the language itself — and that's exactly the problem. The same source code can produce different values for b across different simulators, or even across different runs of the same simulator, with no error or warning at all.

Quick Check

Q: Would adding a $display statement inside Block 2 to print a right before computing b reliably tell you which order actually happened?

Show Answer

It would tell you what happened for that specific simulation run on that specific simulator — but not what will always happen. The whole issue is that this order isn't fixed by the language, so observing it once doesn't guarantee the same order next time, on a different tool, or after an unrelated code change shifts scheduling.

The Fix: Nonblocking Assignments Remove the Ambiguity

Changing both assignments to nonblocking resolves this completely — not by fixing the execution order, but by making the order irrelevant:


always @ (posedge clk) begin
  a <= 5;
end

always @ (posedge clk) begin
  b <= a + 1;
end

Block 2's right-hand side, a, is now evaluated using whatever value a held before this edge — and that's true no matter which of the two blocks happens to run first in Active, because Block 1's write to a doesn't actually take effect until the NBA region, which only runs after every Active-region read has already completed. b now ends up as (a's pre-edge value) + 1, every single time, on every simulator, regardless of scheduling order.

Quick Check

Q: In the nonblocking version, does it matter whether Block 1 or Block 2's Active-region code executes first?

Show Answer

No — that's the entire point. Block 1's actual write is deferred to NBA either way, so Block 2's read in Active always sees the pre-edge value, completely independent of which block's Active-region code happens to run first.

Guidelines for Avoiding Races

These widely-used rules follow directly from the scheduling model:

  • Use nonblocking assignments for sequential (clocked) logic and latches
  • Use blocking assignments for combinational logic
  • Don't mix blocking and nonblocking assignments within the same always block
  • Avoid having more than one always block assign to the same variable
  • When displaying a value driven by a nonblocking assignment, use $strobe or $monitor instead of $display, to guarantee seeing the fully settled value rather than one that depends on Active-region ordering

Testbench-Side Race Avoidance

The same risk exists between testbench and design code, not just within a design's own logic. A testbench that reads a DUT-driven signal using a plain procedural blocking read, at the exact same edge the DUT updates that signal, is exposed to the same kind of ordering ambiguity. The standard fix is to sample through a clocking block, which defines an explicit input skew and guarantees a settled, race-free value — rather than relying on unspecified Active-region ordering between the testbench and the design.

Where Does This Matter in Practice?

Race conditions are one of the most notorious sources of "it worked yesterday" bugs in real verification — a regression can pass reliably for a long time and then fail (or pass differently) purely because an unrelated code change shifted scheduling order enough to flip which side of a race wins. This is exactly why the guidelines above are treated as close to mandatory in most real coding standards, rather than just stylistic preferences.

Common Beginner Mistakes

Mistake #1: Assuming a race that "worked" once is actually fixed

As the first Quick Check shows, observing one specific outcome doesn't mean that outcome is guaranteed. Why? A race can appear to behave consistently for a long time and then produce a different result the moment something shifts scheduling order — passing once is not the same as being correct.

Mistake #2: Mixing blocking and nonblocking assignments in the same always block

This makes the resulting timing much harder to reason about, and is explicitly discouraged by the standard guidelines. Why? Combining the two styles in one block undermines the very consistency that picking one style per block (sequential vs. combinational) is meant to provide.

Mistake #3: Letting more than one always block drive the same variable

Beyond the ordering risk this creates, many tools flag this as a multiple-driver issue on its own. Why? Even setting aside race conditions, having two separate blocks responsible for the same variable is a maintainability problem — it's rarely clear which one is "supposed" to win.

Mistake #4: Reading a DUT-driven signal with a plain blocking read at the same edge it updates

This exposes a testbench to the same ordering ambiguity demonstrated in this article's main example. Why? A clocking block exists specifically to remove this risk, sampling DUT signals at a well-defined, race-free point relative to the clock instead of racing the design directly.

Practice Exercise

Challenge: Identify and Fix a Race

Two always blocks both trigger on posedge clk. One computes sum = x + y;, and a separate block computes avg = sum / 2;. Explain why this is a race condition, and rewrite both blocks to remove it.

Hint

Ask the same question as the article's main example: does the second block's result depend on whether it reads sum before or after the first block updates it?

Solution

always @ (posedge clk) begin
  sum <= x + y;
end

always @ (posedge clk) begin
  avg <= sum / 2;
end

The original version is a race for exactly the same reason as the article's a/b example: avg's block reads sum, which the other block also writes at the same edge, using blocking assignments whose relative order isn't guaranteed. Switching both to nonblocking makes avg always compute from sum's pre-edge value, regardless of which block's Active-region code happens to run first.

Summary

In this tutorial, you learned:

  • A race condition exists when a simulation's outcome depends on an execution order the language doesn't actually guarantee
  • Two blocking assignments to the same variable, from different always blocks triggered by the same event, are a genuine, textbook race — either possible outcome is equally valid according to the language
  • Switching to nonblocking assignments doesn't fix the execution order — it makes that order irrelevant, since the actual write is always deferred to NBA regardless of which block runs first in Active
  • The standard guidelines (nonblocking for sequential logic, blocking for combinational, never mixed, never multiply-driven) all follow directly from this model
  • The same risk applies between testbench and design code — a clocking block is the standard way to sample DUT signals without racing them directly
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5