Background

Very often, we may want to start an individual thread with fork block in a loop. And also very often, if we use looped variables in fork block, things may not work as expected. In the following example, we will see do_something() is always applying to the last looped node in nodes array.

// Example of a bad usage
foreach (nodes[i]) begin
    Node node = nodes[i];
    fork
        do_something(node);
    join_none
end

To solve this issue, two things we should know and understand: fork working mechanism and automatic variable.

When do fork threads start?

It's a kind of tricky must-know. According to SystemVerilog LRM (2023) 9.3.2 Parallel blocks:

In all cases, processes spawned by a fork-join block shall not start executing until the parent process is blocked or terminates.

The key point here is a thread does not start immediately when it reaches fork. For fork-join and fork-join_any it's quite straight-forward because the parent thread is blocked when it reaches join or join_any.

But join_none does not block the parent thread, so in the above example, do_something() would not start in each loop, until they see something blocking after the foreach loop. At that time they starts, the variable node has been assigned to the last looped node for each thread. That's why we see the same results for each thread.

automatic vs static variable

automatic and static are variable lifetime modifiers.

automatic means the variable only lives in current scope, which is the default behavior of many other programming languages. According to SystemVerilog LRM (2023) 6.21 Scope and lifetime:

Class methods and declared for loop variables are by default automatic, regardless of the lifetime attribute of the scope in which they are declared.

static means the variable is re-used among each declaration or access in the same scope, like how the C static works. This is the default lifetime in SystemVerilog.

Back to the example, node is static by default, so each fork context shares the same node variable, which is the last looped node in this case.

// Example of a bad usage
foreach (nodes[i]) begin
    Node node = nodes[i]; // static variable by default, reused in each loop
    fork
        do_something(node);
    join_none
end

To assign and use a new node variable in each loop and each fork context, we should explicitly mark the variable as automatic.

// Best practice
foreach (nodes[i]) begin
    automatic Node node = nodes[i]; // explicit automatic lifetime
    fork
        do_something(node);
    join_none
end

Conclusion

The gap here is the default lifetime is different from other languages like C, and the fork block is not as intuitive as it looks like. I suggest to go through the whole 6.21 Scope and lifetime section to understand the lifetime model.

Read more

Published under Creative Commons Attribution-ShareAlike (CC-BY-SA) license. Feel free to comment or share :)