Source Deep Dive: SC_METHOD, SC_THREAD, and wait()
How process registration, static sensitivity, runnable queues, and wait-based suspension work conceptually.
How to Read This Lesson
This is a source-reading lesson. We will use the Accellera implementation as a microscope, while keeping the LRM as the portability contract.
SC_METHOD and SC_THREAD look like simple macros, but they create very different process objects inside the kernel.
The implementation work behind those lines is the heart of SystemC execution according to the LRM.
Source and LRM Trail
This lesson is deliberately source-facing. Use Docs/LRMs/SystemC_LRM_1666-2023.pdf to decide what must be portable, then use .codex-src/systemc/src/sysc and .codex-src/systemc/src/tlm_core to see one reference implementation. Treat private members as explanatory, not as APIs your models should depend on.
What Registration Must Capture
When a process is registered, the kernel needs the owning module, the member function pointer, the process kind, sensitivity, and execution state.
To prove how the kernel manages these different process types and runnable queues, here is a complete compilable example demonstrating static sensitivity and wait dynamics.
#include <systemc>
using namespace sc_core;
SC_MODULE(ProcessInternalsDemo) {
sc_event trigger_event;
SC_CTOR(ProcessInternalsDemo) {
// 1. SC_METHOD: A non-resumable callback
SC_METHOD(method_process);
sensitive << trigger_event;
dont_initialize();
// 2. SC_THREAD: A resumable coroutine with execution state
SC_THREAD(thread_process);
}
// Conceptually similar to:
// void run_once() { (owner->*callback)(); }
void method_process() {
std::cout << "[METHOD] Executing at " << sc_time_stamp() << "\n";
// wait(); // ILLEGAL: Methods cannot suspend!
}
// Conceptually similar to a fiber context switch
void thread_process() {
std::cout << "[THREAD] Started at " << sc_time_stamp() << "\n";
// Transfers control back to scheduler, removing process from runnable queue
wait(10, SC_NS);
std::cout << "[THREAD] Resumed at " << sc_time_stamp() << ". Firing event.\n";
trigger_event.notify(SC_ZERO_TIME);
}
};
int sc_main(int argc, char* argv[]) {
ProcessInternalsDemo demo("demo");
sc_start(50, SC_NS);
return 0;
}SC_METHOD vs SC_THREAD Internals
An SC_METHOD process runs to completion. It cannot call wait(), because the kernel does not preserve a suspended stack for it. It is merely a C++ callback function invoked by the kernel.
An SC_THREAD process can suspend. The kernel must resume this process after the wait condition is satisfied. That requires process state beyond a simple callback. Depending on the platform, the implementation uses coroutine or fiber-like mechanisms.
wait() Changes Process State
wait() does not sleep the host OS thread. It tells the SystemC scheduler: this process is no longer runnable until a condition is met.
Each call to wait() installs a wait condition, removes the process from the runnable set, and transfers control back to the scheduler.
Runnable Queues
At simulation time, the scheduler keeps track of runnable processes. A simplified view:
- Pop process from runnable queue.
- Transfer control to the process (
run()or context switch). - Process returns or yields (
wait()). - Once runnable queue is empty, trigger the update phase.
When you know the internal shape, the rules stop feeling arbitrary. SystemC lets you write ordinary-looking C++, but the kernel is building a small event-driven operating environment around your objects.
Exhaustive Deep Dive: IEEE 1666-2023 LRM and Accellera Process Architecture
To truly understand how SystemC executes concurrently, we must look at IEEE 1666-2023 LRM Section 5 (Processes) and how the Accellera kernel implements these specifications in src/sysc/kernel.
LRM Section 5: Process Semantics
The LRM defines two primary simulation processes: Methods and Threads (and the deprecated SC_CTHREAD).
- LRM Clause 5.2.2 (Method processes): Executes from start to finish without blocking. It returns control to the simulator.
- LRM Clause 5.2.3 (Thread processes): May call
wait()to suspend execution. Its local variables and execution state are preserved across suspensions.
The LRM specifies that the simulator determines which processes to execute during the Evaluation Phase (LRM 4.2.1.2) based on the set of runnable processes.
Inside sysc/kernel: sc_process_b Base Class
Both SC_METHOD and SC_THREAD expand to macros that dynamically allocate process objects. In the Accellera source (src/sysc/kernel/sc_process.h), all process types inherit from sc_process_b.
class sc_process_b {
// ...
sc_process_state m_state; // e.g., RUNNABLE, SLEEPING, TERMINATED
sc_event_list* m_event_list; // What this process is dynamically waiting on
sc_process_b* m_runnable_nxt; // Intrusive linked list for the scheduler
// ...
};When an event occurs (e.g., trigger_event.notify()), the kernel checks its list of sensitive processes. If a process matches, the kernel pushes it onto the sc_runnable list (defined in sc_runnable.h). The scheduler's evaluation loop just pops items off this intrusive linked list and executes them.
sc_method_process vs sc_thread_process
In src/sysc/kernel/sc_method_process.h, the implementation is surprisingly simple. When a method is popped from the runnable queue, the kernel essentially calls a C++ member function pointer:
inline void sc_method_process::execute() {
m_semantics_method->invoke( m_semantics_host );
}Because it is a standard C++ function call, the C++ call stack is used. If you try to call wait() inside a method, the kernel's wait implementation checks sc_get_current_process_b()->process_kind() and throws an SC_ID_WAIT_NOT_ALLOWED_ exception because there is no mechanism to save the stack.
In src/sysc/kernel/sc_thread_process.h, the implementation is vastly different. A thread requires a coroutine.
QuickThreads and User-Space Context Switching
SystemC cannot map SC_THREAD to std::thread or POSIX pthread because OS-level context switching is non-deterministic and takes thousands of cycles per switch. Hardware models may have millions of threads context-switching every simulated nanosecond.
Instead, the Accellera kernel implements QuickThreads (located in src/sysc/qt/). QuickThreads is a library of bare-metal assembly code (qt/md/) that implements cooperative user-space threads (fibers/coroutines).
When you call wait(10, SC_NS) in an SC_THREAD:
- The kernel adds the thread to a time-based queue (
sc_simcontext::m_time_events). - The thread invokes
sc_cor_qt::yield(). - The underlying QuickThreads assembly code executes. For x86_64 (
src/sysc/qt/md/i386.sor similar), it pushes the instruction pointer (RIP), base pointer (RBP), and all callee-saved registers onto the thread's own allocated stack memory. - It then loads the
sc_simcontextscheduler's saved stack pointer into the CPU's stack pointer register (RSP). - Execution jumps back to the scheduler loop.
When the 10 ns elapse, the scheduler pops the thread, does the reverse context switch (swapping RSP back to the thread's stack), and pops the CPU registers. The C++ code resumes seamlessly exactly where wait() was called.
Dynamic Sensitivity and the sc_event_or_list
When you write wait(e1 | e2), you create dynamic sensitivity (LRM 5.2.14).
In the source, operator| on sc_event objects returns an sc_event_or_list. The kernel stores this list inside m_event_list of the sc_thread_process. When either event fires, the kernel checks this list, marks the thread as runnable, and clears the list. This is why you must re-issue wait(e1 | e2) in a while loop if you want to wait again.
Understanding this architecture is crucial: SC_METHOD is a fast, C++ callback. SC_THREAD is a cooperative assembly-level coroutine with an isolated stack. Choose wisely based on the simulation performance required.
Comments and Corrections