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:

  1. Initialize — run all processes once to set initial state
  2. Evaluate — run all processes that are ready (triggered by events)
  3. Update — apply pending channel writes (delta cycle)
  4. Advance Time — move to the next scheduled event
  5. Repeat from Evaluate
Key insight The kernel cannot preempt a running process. If a process enters an infinite loop without yielding, simulation freezes. This is why the mechanism of yielding — which differs completely between SC_THREAD and SC_METHOD — is the most important thing to understand.

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 ...
  }
}
Common SC_THREAD mistake — deadlock An SC_THREAD that loops forever without at least one 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.

Common SC_METHOD mistake — calling wait() Calling 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
The missed event problem Immediate 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:

Use SC_METHOD when:

Quick decision rule Ask yourself: "Does this process need to pause partway through and remember where it was?" If yes → SC_THREAD. If it reacts to events and completes in one shot → SC_METHOD.

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.