SC_THREAD vs SC_METHOD — The Real Difference
Most SystemC beginners treat SC_THREAD and SC_METHOD as interchangeable styles — pick one and stick with it. That's the wrong mental model, and it leads to some of the most confusing simulation bugs you'll encounter: processes that deadlock silently, events that get missed, or runtime crashes with no obvious cause.
The real difference isn't about length or complexity of code. It's about one thing: whether a process can suspend mid-execution and resume from where it left off. Everything else flows from that.
1. How the Simulation Kernel Actually Works
Before comparing the two, you need to understand what the kernel is doing underneath. SystemC uses cooperative multitasking — not true parallelism. Only one process runs at a time. When a process runs, it has full control of the CPU until it voluntarily yields back to the kernel.
The kernel runs in a loop:
- Initialize — run all processes once to set initial state
- Evaluate — run all processes that are ready (triggered by events)
- Update — apply pending channel writes (delta cycle)
- Advance Time — move to the next scheduled event
- Repeat from Evaluate
2. SC_THREAD — The Suspendable Process
An SC_THREAD is started exactly once at the beginning of simulation. It runs, can suspend itself with wait(), and resumes from the exact same line when the wait condition is satisfied. Local variables on the stack persist across suspensions — the process "remembers" where it was.
SC_MODULE(blinker) {
sc_out<bool> led;
SC_CTOR(blinker) {
SC_THREAD(blink_thread);
}
void blink_thread() {
while (true) { // loop forever — OK in SC_THREAD
led.write(true);
wait(300, SC_MS); // suspend for 300ms, resume here
led.write(false);
wait(300, SC_MS); // suspend again
}
}
};
Notice: the loop state and the LED toggle sequence are naturally expressed as sequential code. The process "lives" between calls to wait().
wait() — All the Forms
wait() is what makes SC_THREAD powerful. It has many variants:
wait(10, SC_NS); // pause for 10 nanoseconds wait(event); // wait until event fires wait(e1 | e2); // wait for either event wait(e1 & e2); // wait until both have fired wait(10, SC_NS, event); // timeout OR event, whichever first wait(); // use static sensitivity list
Static Sensitivity for SC_THREAD
You can also define sensitivity statically in the constructor, then use a bare wait() inside the process to wait on it:
SC_CTOR(my_module) {
SC_THREAD(my_thread);
sensitive << clk.pos(); // static sensitivity: rising edge
}
void my_thread() {
while (true) {
wait(); // waits for clk posedge each time
// ... do work ...
}
}
wait() will never yield control. The simulation kernel stalls. Always ensure every path through an infinite loop passes through a wait().
3. SC_METHOD — The Event-Driven Process
An SC_METHOD works completely differently. It is re-invoked by the scheduler every time its trigger condition fires. Each invocation runs to completion and returns. There is no suspension, no resumption — the process starts from scratch each time.
Because of this, SC_METHOD processes cannot call wait(). Attempting to do so is a runtime error. State that needs to persist between invocations must be stored in module member variables, not local stack variables.
SC_MODULE(edge_counter) {
sc_in<bool> clk;
sc_out<int> count;
int m_count = 0; // member variable — persists
SC_CTOR(edge_counter) {
SC_METHOD(count_method);
sensitive << clk.pos(); // trigger on rising edge
}
void count_method() {
m_count++; // called fresh on every posedge
count.write(m_count);
// no wait() here — would crash at runtime
}
};
next_trigger() — Dynamic Sensitivity
While SC_THREAD uses wait(event) to change what it's waiting for at runtime, SC_METHOD uses next_trigger() to control what event triggers the next invocation:
void smart_method() {
if (mode == IDLE) {
next_trigger(start_event); // wait for start next time
} else if (mode == ACTIVE) {
next_trigger(10, SC_NS); // re-trigger in 10ns
} else {
next_trigger(done_event | err_event); // either fires next
}
}
If you don't call next_trigger(), the static sensitivity list applies again automatically. If there is no static sensitivity and no next_trigger() call on some code path, that path will never re-trigger — a silent bug.
wait() directly or through any function that calls wait() internally will crash with a runtime error: "wait() is only allowed in SC_THREAD". Blocking channel calls like sc_fifo::read() and sc_fifo::write() also call wait() internally — use the non-blocking nb_read() / nb_write() variants instead.
4. Events and Notifications — What Both Types Share
Both SC_THREAD and SC_METHOD respond to events. An sc_event is a point in time — it has no value and no duration. When notified, it wakes any process waiting on it.
sc_event my_event; // Notify forms: my_event.notify(); // immediate — current evaluate phase my_event.notify(SC_ZERO_TIME); // delta delay — next evaluate phase my_event.notify(10, SC_NS); // timed — 10ns from now my_event.cancel(); // cancel a pending notification
notify()) can be missed. If a process has not yet reached its wait(event) when the event fires, the process never sees it. Use notify(SC_ZERO_TIME) to defer to the next delta cycle — by then all processes have had a chance to reach their wait points.
5. Side-by-Side Comparison
| Feature | SC_THREAD | SC_METHOD |
|---|---|---|
| Started by kernel | Once, at simulation start | On every triggering event |
| Can call wait() | ✅ Yes — suspends & resumes | ❌ No — runtime error |
| Stack variables | Persist across wait() calls | Reset on each invocation |
| State storage | Local variables (stack) | Module member variables only |
| Dynamic sensitivity | wait(event) | next_trigger(event) |
| Static sensitivity | sensitive + wait() | sensitive (auto re-triggers) |
| Blocking channel calls | ✅ OK (sc_fifo::read/write) | ❌ No — use nb_read/nb_write |
| Performance | Slightly heavier (stack context) | Slightly faster (no stack save) |
| Best for | Protocols, state machines, stimulus | Combinational logic, monitors, RTL |
| HDL equivalent | VHDL process with wait statements | Verilog always @, VHDL concurrent stmt |
| Classic mistake | Infinite loop without wait() → deadlock | Calling wait() → crash |
6. When to Use Which
Use SC_THREAD when:
- Your logic is naturally sequential — "do A, wait, do B, wait, do C"
- You're modeling a protocol handshake, bus transaction, or state machine with phases
- You need a stimulus generator that produces timed sequences
- You're reading from an
sc_fifo(blocking read is natural here) - Your process needs to remember where it was between time steps
Use SC_METHOD when:
- You're modeling combinational logic — output is a pure function of inputs
- You're writing a monitor or checker that reacts to signal changes
- You're modeling an RTL flip-flop or latch (sensitive to clock edge)
- Performance matters — SC_METHOD avoids stack save/restore overhead
- The process is stateless or state fits naturally in module members
7. A Practical Example — Same Problem, Both Ways
A simple timeout detector: raise an alarm if a signal stays low for more than 100ns.
SC_THREAD approach — natural sequential logic
void timeout_thread() {
while (true) {
wait(signal.negedge_event()); // wait for signal to go low
wait(100, SC_NS, signal.posedge_event()); // 100ns or posedge
if (!signal.read()) { // still low after 100ns?
alarm.write(true);
wait(signal.posedge_event()); // wait for recovery
alarm.write(false);
}
}
}
SC_METHOD approach — same logic, but state must be explicit
bool m_timing = false;
sc_time m_low_since;
void timeout_method() {
if (!signal.read() && !m_timing) {
m_timing = true;
m_low_since = sc_time_stamp();
next_trigger(100, SC_NS); // re-check in 100ns
} else if (!signal.read() && m_timing) {
alarm.write(true); // still low — alarm
m_timing = false;
next_trigger(signal.posedge_event());
} else {
alarm.write(false); // signal recovered
m_timing = false;
next_trigger(signal.negedge_event());
}
}
Both work. The SC_THREAD version reads like a protocol spec. The SC_METHOD version is more explicit about state — which is both its strength (clear module state) and its burden (you manage every transition).
Bonus: sc_spawn — Dynamic Processes
SystemC 2.1 introduced a third option: sc_spawn() — creating processes at runtime during simulation, not just at elaboration. Spawned processes are SC_THREAD-style by default but can accept arguments and return values, which static processes cannot.
#define SC_INCLUDE_DYNAMIC_PROCESSES
#include <systemc>
void monitor_transaction(sc_fifo<Packet>& fifo) {
Packet p = fifo.read();
cout << "Got packet @ " << sc_time_stamp() << endl;
}
// In an SC_THREAD:
sc_process_handle h = sc_spawn(
sc_bind(&monitor_transaction, sc_ref(my_fifo))
);
wait(h.terminated_event()); // wait for spawned process to finish
sc_spawn is particularly useful for fork-join parallelism — spawning multiple concurrent checkers and waiting for all to complete with SC_FORK/SC_JOIN. More on that in a future article.
The choice between SC_THREAD and SC_METHOD is not about preference — it's about which execution model fits the behavior you're modeling. Hardware that reacts combinationally to inputs is an SC_METHOD. A protocol that sequences through phases over time is an SC_THREAD. Get that mapping right, and simulation bugs become rare.