Cleaner way to kill a fork/join thread in SystemVerilog
Managing parallel threads is a common challenge in SystemVerilog testbenches. When you spawn threads using fork/join, fork/join_any, or fork/join_none, you often need to terminate them gracefully. This post covers three approaches, with a focus on the cleanest method using the process class.
Table of Contents
- Method 1: Using process::self() and kill() (Recommended)
- Method 2: Using disable fork
- Method 3: Using disable with labels
- Comparison Table
- Best Practices
Method 1: Using process::self() and kill() (Recommended)
The process class in SystemVerilog provides a clean, object-oriented way to manage threads. This is the preferred approach in UVM environments and offers the most control.
process thread_handle;
// Spawn a thread
fork
begin : worker_thread
// Store handle to this process
thread_handle = process::self();
// Your tasks here
drive_transaction();
monitor_response();
end
join_none
#0; // Allow the fork to start (important!)
// Later, when you need to kill the thread
wait(stop_condition);
if (thread_handle != null && thread_handle.status() != process::FINISHED)
thread_handle.kill();
Key Points:
process::self()returns a handle to the current process- The
#0delay is critical forfork/join_none— without it, the thread may not start before you try to access the handle kill()terminates the process and all its child processes- Always check
status()before killing to avoid errors
Process Status Values
| Status | Description |
|---|---|
process::FINISHED | Process completed normally |
process::RUNNING | Process is currently executing |
process::WAITING | Process is waiting on a blocking statement |
process::SUSPENDED | Process is suspended |
process::KILLED | Process was terminated by kill() |
Method 2: Using disable fork
The disable fork statement kills all child processes spawned by the current thread. It's simple but can be dangerous if not used carefully.
fork
begin
task_a();
end
begin
task_b();
end
join_none
#100ns;
disable fork; // Kills ALL child threads from this parent
Warning: disable fork affects all spawned threads from the calling process, not just the ones you might want to stop. This can cause unintended side effects in complex testbenches.
Method 3: Using disable with labels
You can disable a specific named block using its label. This is more targeted than disable fork.
fork
begin : thread_A
forever begin
do_work_a();
end
end
begin : thread_B
forever begin
do_work_b();
end
end
join_none
#500ns;
disable thread_A; // Only kills thread_A, thread_B continues
Limitation: The label must be visible in the current scope, which can be problematic in modular code.
Comparison Table
| Method | Precision | UVM Compatible | Scope Requirement | Best For |
|---|---|---|---|---|
process::kill() |
Single thread | Yes | Handle only | UVM components, complex testbenches |
disable fork |
All children | Risky | Current process | Simple, isolated forks |
disable label |
Named block | Limited | Label visibility | Module-level threads |
Best Practices
- Always use
#0afterfork/join_none— This ensures the spawned thread gets a chance to execute and store its handle. - Check process status before killing — Avoid calling
kill()on already finished processes. - Use process class in UVM — The UVM library uses this pattern internally (see
uvm_sequence_base). - Store handles in queues for multiple threads:
process thread_q[$];
for (int i = 0; i < 5; i++) begin
fork
automatic int idx = i;
begin
thread_q.push_back(process::self());
worker_task(idx);
end
join_none
end
#0;
// Kill all threads later
foreach (thread_q[i]) begin
if (thread_q[i].status() != process::FINISHED)
thread_q[i].kill();
end
Conclusion
The process class provides the cleanest and most flexible way to manage thread lifecycles in SystemVerilog. It's the standard approach in UVM and gives you fine-grained control over individual threads without affecting others.
For more SystemVerilog tips, check out my other posts on SystemVerilog and UVM.
Comments (0)
Leave a Comment