About Systemverilog process and fork join
In Systemverilog, we can group statements into blocks and there are two ways to do so. The first way is groups them into begin
-end
block, where statements are executed sequentially. The other way is to use the fork
-join
block, also called parallel block. In this block, all statements are executed concurrently. This post will share how to use this fork
-join
block and some of its practical cases.
join/join_none/join_any
First up, let’s look at the structure of the parallel block. The block begin with the keywork fork
, all procedure statements under this keywork will be started at the same time. When the parent process can resume its execution is depended on the closing keywork. We have join
, join_none
and join_any
.
- For
fork
-join
, all the procedure statements will have to finish before the parent process can resume its execution. - For
fork
-join_any
, the parent process will be blocked until one of the processes spawned by the fork finished. - For
fork
-join_none
, the parent process will continue at the same time with all the processes spawned by the fork.
Just simple as that, start your block with fork
, then end your block with either join
, join_any
or join_none
. However, life is not that much easy. Let’s consider some cases below, where using some other control methods alongside fork is necessary.
fork join in a loop
fork join_none in a loop
Let consider this case, we have a list of item, and we want to start a single procedure statement for each item of that list, and we want all of those procedure statements start at the same time. Also, we need all of those processes to finish before executing any other statement. We can easily achieve the requirement using fork
and join_none
as below.
int lst[5] = '{1,2,3,4,5};
for(int i=0; i < 5; i++ ) begin
fork
automatic int j = i;
begin
$display ("%t ps, start thread %d", $time, j);
#lst[j];
$display("%t ps, end of thread %d", $time,j);
end
join_none
end
wait fork;
#1;
$display("the NEXT Statement ... ");
There are several things that we can notice here:
- Firstly it’s the
automatic
keywork. We need to copyi
toj
automatic variable in each iteration of the for loop. Since we usejoin_none
here, all of 5 processes will start at the same time, and we only have onei
variable, and after 5 iterations,i
will hold a value 4. This means that if we usei
variable instead of creating local copy of it, these all 5 processes will run with the same value ofi
after 5 iterations, which is 4. Then we’ll end up having 5 exactly the same processes instead of 5 processes with 5 different values of a list. - Secondly, it’s the
wait fork
statement, this is for waiting all 5 processes to finish before executing the next statement. - Why don’t we use
fork/join
here? It’s simply because when usingjoin
instead ofjoin_none
inside a loop, all the processes insidefork/join
will have to finish before moving to the next iteration of the loop. In the example above, if thefork/join
is used instead offork/join_none
, we’ll have 5 thread executed sequentially, not concurrently. - Note that the
automatic int j = i;
statement must be afterfork
keywork. Let’s check below example, thej
variable will be 4 in all processes:
...
for(int i=0; i < 5; i++ ) begin
fork
begin
automatic int j = i; // inside a begin-end
// the j is only assign value of i after the begin block start
// however this begin block only start after for loop finished,
// eventually j will hold the value 4 for all proccesses
...
end
join_none
end
...
fork join_any in a loop
Similar to the example above, but this time, we need to execute the next statement $display("the NEXT Statement ... ");
right after ONE of the 5 processes finished. In this case, using fork/join_any
will NOT solve the requirement.
int lst[5] = '{1,2,3,4,5};
for(int i=0; i < 5; i++ ) begin
fork
automatic int j = i;
begin
$display ("%t ps, start thread %d", $time, j);
#lst[j];
$display("%t ps, end of thread %d", $time,j);
end
join_any // --> wait until one of the processes inside the fork/join_any finished
end
wait fork;
#1;
$display("the NEXT Statement ... ");
- Why it does not work? What is required is to start 5 processes at the same time using a for loop, then executing the next statement right after any one of the processes finished. Here inside the
fork/join_any
, there is only 1 procedure statment (insidebegin/end
), therefore we will need to wait for the process to finish before moving to the next iteration of the loop, which means we will have 5 processes executed sequentially, not concurrently. - To solve this problem, we need to use
fork/join_none
inside loop to create 5 processes executed concurrently, and then using an event to execute the next statement right after one of the 5 processes finished.
int lst[5] = '{1,2,3,4,5};
uvm_event finish_event;
for(int i=0; i < 5; i++ ) begin
fork
automatic int j = i;
begin
$display ("%t ps, start thread %d", $time, j);
#lst[j];
$display("%t ps, end of thread %d", $time,j);
finish_event.trigger(); //
end
join_none
end
finish_event.wait_trigger(); // wait for an event to be triggered instead of using wait fork;
#1;
$display("the NEXT Statement ... ");
- In the above example, by using
fork/join_none
, we have 5 processes executed concurrently. And by usinguvm_event
, the$display("the NEXT Statement ... ");
will be executed when one of the 5 processes finished.
fork-join in a forever loop
We can also put the fork
block inside a forever loop. However, we should be careful about statements inside forever loop, because they might hang our simulator. Never write any code with no statement to control the process in forever loop like below:
forever begin
fork
begin
$display ("%t ps, start thread %d", $time, j);
$display("%t ps, end of thread %d", $time,j);
end
join
end
- The above code will hang our simulator. The 2
$display
tasks will be executed right away, then move to the next interation of the forever loop. This loop will run continuously and hang the simulator. We should at least have some control inside thefork-join
like below:
forever begin
fork
begin
$display ("%t ps, start thread %d", $time, j);
#10;
////
// Wait for some signal to trigger.
////
$display("%t ps, end of thread %d", $time,j);
end
join
end
Also, be careful when using fork/join_none
in forever loop as well . It will also hang our simulator since infinite processes will be created unless we have some process control inside the loop.
forever begin
fork
begin
$display ("%t ps, start thread %d", $time, j);
#10;
//
// Wait for some signal to trigger.
...
//
$display("%t ps, end of thread %d", $time,j);
end
join_none
wait fork;
//--> if does not have process control such as this wait fork;
// the forever loop will create infinite processes and hang simulator
end
forever loop inside fork-join_none
By putting forever loop inside fork
-join_none
we need to aware that there will be a subprocess created for this forever loop. And if we do not have the control to break out of this forever loop, then the task monitor_signal
in below example will never return the control.
task monitor_signal();
fork
forever begin
wait (mod_a_if.signal_a);
// other statements
// ...
end
join_none
endtask: monitor_signal
This could cause an issue in some cases. Consider this case below, when using above task in a uvm task like main_phase
:
// inside uvm component
task main_phase(uvm_phase phase);
phase.raise_objection(this);
//call above monitor task
monitor_signal();
phase.drop_objection(this);
endtask
- In uvm, objections are used to synchronized consumed-time phases of all uvm components.
raise_objection
will increase objection counter by one,drop_objection
will decrease objection counter by one. When the objection counter is zero, all uvm_component will move to next phase at the same time. - So to be able to move to next phase, in this example, we need to be able to drop the objection that has been raised at the start of this
main_phase
. - However, as mentioned in the previous example, the
monitor_signal()
task contains a forever loop insidefork
-join_none
. And this task will not return the control since the forever will keep running forever. Because of that, the control will not return to themain_phase()
task, and thephase.drop_objection()
will never be called to drop the objection. This causes the simulator to stuck at thismain_phase
. - To solve this issue, we can use the
disable
statement for this forever loop, or usefine-grain process control
to kill this forever process. Let’s check below section.
Process control
Systemverilog supplies us several ways to control processes, those methods are especially useful when it comes to using fork/join
statement. Besides, we can also use other methods such as uvm_event
as above to tackle the problem.
wait fork statement
wait fork
might be the statement that is used the most when controlling processes in fork join
. It’s pretty simple, all the child subprocesses will have to finish before executing the next statement.
The child subprocesses are the processes started by the current process.
fork
#1 $display("%t ps, end of thread %d", $time,j);
#3 $display("%t ps, end of thread %d", $time,j);
#2 $display("%t ps, end of thread %d", $time,j);
join_any
$display("the first process has finished");
wait fork; // wait for all the processes inside fork/join_any to finished
$display("the NEXT Statement ... ");
disable fork statement
disable fork
, in the other hand, will terminates all active descendants (child subprocesses).
fork
#1 $display("%t ps, end of thread %d", $time,j);
#3 $display("%t ps, end of thread %d", $time,j);
#2 $display("%t ps, end of thread %d", $time,j);
join_any
$display("the first process has finished");
disable fork; // disable all the processes inside fork/join_any after the first subprocesses finished.
$display("the NEXT Statement ... ");
disable statement
We also have a disable
statement, which can disable the processes of the entire block or task (cannot be used to disable a function).
Let’s use above example with new requirement: disable all remaining active process after one of the processes finished.
We have a list of item, and we want to start a single procedure statement for each item of that list and we want all of those procedure statement start at the same time.
int lst[5] = '{1,2,3,4,5};
uvm_event finish_event;
for_loop_block_1: for(int i=0; i < 5; i++ ) begin
fork
automatic int j = i;
begin
$display ("%t ps, start thread %d", $time, j);
#lst[j];
$display("%t ps, end of thread %d", $time,j);
finish_event.trigger(); //
end
join_none
end
finish_event.wait_trigger();
disable for_loop_block_1; //
#1;
$display("the NEXT Statement ... ");
The disable block statement can even disable itself:
for_loop_block_1: for(int i=0; i < 5; i++ ) begin
fork
automatic int j = i;
begin
$display ("%t ps, start thread %d", $time, j);
#lst[j];
$display("%t ps, end of thread %d", $time,j);
disable for_loop_block_1; // disable itself
end
join_none
end
wait fork;
#1;
$display("the NEXT Statement ... ");
- Here we have the for loop an block name,
for_loop_block_1
, then when the first process finished, we kill all remaining active processes by usingdisable for_loop_block_1
.
fine-grain process control
Systemverilog also provide us this built-in process
class (in the built-in std
package) to control the active processes. Mostly, I use this process
class for debugging purpose, to check the status of all the processes in the fork/join
block. Check the example below:
std::process p1,p2,p3;
initial begin
fork
begin
$display("%t ps, start of thread %d", $time,1);
p1 = std::process::self();
#1
$display("%t ps, end of thread %d", $time,1);
end
begin
$display("%t ps, start of thread %d", $time,2);
p2 = std::process::self();
#3
$display("%t ps, end of thread %d", $time,2);
end
begin
$display("%t ps, start of thread %d", $time,3);
p3 = std::process::self();
#2
$display("%t ps, end of thread %d", $time,3);
end
join_any
$display("the first process has finished");
$display("thread 1 process status: %s", p1.status().name());
$display("thread 2 process status: %s", p2.status().name());
$display("thread 3 process status: %s", p3.status().name());
wait fork; // wait for all the processes inside fork/join_any to finished
$display("the NEXT Statement ... ");
end
- The p1, p2, p3 has the variable type as
process
class, then we use the static functionself()
of theprocess
class to get the handle to the current processes of thread 1, 2, 3 respectively. - After that, we can use the
status()
method to check the status of the process. - We can also have other control over the process using these built-in methods of the
process
class:kill()
,suspend()
,resume()
, etc.
Let’s take another example of using fine grain process control with forever loop:
std::process m_process_q[$];
...
fork
forever begin
@(posedge signal_a);
fork
m_process_q.push_back(std::process::self());
//... other statements
join_none
end
join_none
...
foreach (m_process_q[i]) begin
m_process_q[i].kill();
end
//
- In above example, we get all the handles of any process created in the forever loop and store in the queue
m_process_q[$]
. - Then later, when necessary, by iterating through that queue, we can kill all those processes.
Finding more information
To have more understanding as well as more examples, you can check the IEEE Standard for Systemverilog, chapter.9 Process.
[Tags
systemverilog
]