Deep Dive: async_request_update and External Thread Integration
Examine the SystemC scheduler mechanics for handling OS-level threads, interrupts, and asynchronous updates using async_request_update.
How to Read This Lesson
These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.
Deep Dive: async_request_update and External Thread Integration
One of the most complex challenges in Virtual Platform (VP) modeling is interfacing the deterministic, cooperative SystemC simulation kernel with the chaotic, preemptive world of the host Operating System (OS).
Imagine a SystemC model that needs to react to external physical stimuli:
- A POSIX
std::threadlistening on a TCP/IP socket for a debugger connection. - An external UI framework (like Qt) sending user button presses to a mocked GPIO pin.
- A real hardware interrupt arriving via a PCIe driver wrapper.
You cannot simply call sc_event::notify() or request_update() from these external OS threads. The IEEE 1666 SystemC kernel is explicitly not thread-safe. Calling kernel APIs from an external OS thread will cause data races, corrupt the scheduler's event queues, and result in segmentation faults.
To solve this, the LRM provides sc_prim_channel::async_request_update(). This tutorial dives into how the Accellera source code implements this via Host OS semaphores and how you can safely build external bridges.
Source and LRM Trail
Advanced core behavior should always be checked against Docs/LRMs/SystemC_LRM_1666-2023.pdf before source details. For implementation, read .codex-src/systemc/src/sysc/kernel and .codex-src/systemc/src/sysc/communication, especially the scheduler, events, object hierarchy, writer policy, report handler, and async update path.
The Kernel Reality: async_request_update()
In the standard primitive channel pattern, request_update() appends the channel pointer directly to sc_simcontext::m_update_list. This list is not protected by mutexes.
Introduced in SystemC 2.3, async_request_update() provides a thread-safe mechanism. If you look at src/sysc/kernel/sc_simcontext.cpp, the kernel maintains a separate queue called m_async_update_list and a host OS synchronization primitive (typically an sc_host_mutex and an sc_host_semaphore).
When an external thread calls async_request_update():
- The kernel acquires the
sc_host_mutex. - It pushes the
sc_prim_channel*intom_async_update_list. - It releases the mutex.
- It signals the
sc_host_semaphore. If the SystemC kernel thread was sleeping (blocked insc_pause()or waiting for the next timed event), this semaphore wakes the OS thread running the SystemC scheduler.
Inside the SystemC thread, during the transition into the next evaluation phase, sc_simcontext::crunch() calls a method to check pending_async_updates(). If true, the kernel safely locks the mutex, moves elements from m_async_update_list into the standard m_update_list, and then calls the update() method of your channel—this happens safely inside the SystemC kernel thread, free from race conditions.
Designing an Asynchronous Channel Bridge
To cross the boundary from OS thread to SystemC thread, you must design a custom primitive channel inheriting from sc_prim_channel.
Step 1: The LRM Rules
- Your channel must inherit from
sc_prim_channel. - The external OS thread must interact with the channel via custom thread-safe queues or atomic variables.
- Once the OS thread alters the shared data, it calls
async_request_update(). - The channel overrides the virtual
update()method. Insideupdate(), you are back in the SystemC thread, so it is safe to pop the shared data and callsc_event::notify().
Step 2: The End-to-End Implementation
The following 100% LRM-compliant code demonstrates a custom channel that listens for "network packets" coming from an external std::thread.
#include <systemc>
#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <chrono>
// -------------------------------------------------------------------------
// 1. The Custom Primitive Channel
// -------------------------------------------------------------------------
class AsyncNetworkBridge : public sc_core::sc_prim_channel {
private:
std::mutex m_mutex;
std::queue<int> m_packet_queue;
sc_core::sc_event m_packet_event;
public:
explicit AsyncNetworkBridge(sc_core::sc_module_name name)
: sc_core::sc_prim_channel(name) {}
// ---------------------------------------------------------
// API for the External OS Thread (e.g., std::thread)
// ---------------------------------------------------------
void inject_packet_from_os_thread(int packet_id) {
{
// Protect our custom queue with a standard C++ mutex
std::lock_guard<std::mutex> lock(m_mutex);
m_packet_queue.push(packet_id);
}
// CRITICAL: Notify the SystemC kernel asynchronously.
// The Accellera kernel locks its sc_host_mutex and adds 'this'
// to m_async_update_list.
this->async_request_update();
}
// ---------------------------------------------------------
// SystemC Kernel Update Phase Callback
// ---------------------------------------------------------
// The kernel moves 'this' to m_update_list and calls update()
// safely from the primary SystemC thread context.
void update() override {
bool has_data = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
has_data = !m_packet_queue.empty();
}
if (has_data) {
// It is now perfectly safe to notify SystemC events!
m_packet_event.notify(sc_core::SC_ZERO_TIME);
}
}
// ---------------------------------------------------------
// API for SystemC Modules
// ---------------------------------------------------------
const sc_core::sc_event& default_event() const {
return m_packet_event;
}
bool read_packet(int& out_packet) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_packet_queue.empty()) return false;
out_packet = m_packet_queue.front();
m_packet_queue.pop();
return true;
}
};
// -------------------------------------------------------------------------
// 2. The SystemC Consumer Module
// -------------------------------------------------------------------------
class Processor : public sc_core::sc_module {
public:
AsyncNetworkBridge* bridge;
SC_HAS_PROCESS(Processor);
Processor(sc_core::sc_module_name name) : sc_core::sc_module(name) {
SC_THREAD(process_packets);
}
void process_packets() {
while (true) {
// Wait for the bridge to notify us of a new packet
wait(bridge->default_event());
int packet;
while (bridge->read_packet(packet)) {
std::cout << "@" << sc_core::sc_time_stamp()
<< " Processor handled packet ID: " << packet
<< std::endl;
}
}
}
};
// -------------------------------------------------------------------------
// 3. sc_main and Thread Lifecycle Management
// -------------------------------------------------------------------------
int sc_main(int argc, char* argv[]) {
sc_core::sc_report_handler::set_actions("/IEEE_Std_1666/deprecated", sc_core::SC_DO_NOTHING);
// Instantiate our custom async bridge
AsyncNetworkBridge network_bridge("network_bridge");
// Instantiate consumer
Processor cpu("cpu");
cpu.bridge = &network_bridge;
// Launch the external OS thread
// This simulates an external system (like a TCP listener)
std::thread external_listener([&]() {
for (int i = 1; i <= 3; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::cout << "[OS Thread] Injecting packet " << i << std::endl;
network_bridge.inject_packet_from_os_thread(i);
}
});
std::cout << "Starting SystemC simulation..." << std::endl;
// Run simulation. We use sc_start() with a specific time because
// waiting on async events indefinitely can cause the kernel to starve
// if there are no native SystemC events pending.
sc_core::sc_start(200, sc_core::SC_MS);
std::cout << "Simulation finished." << std::endl;
// Clean up OS thread
if (external_listener.joinable()) {
external_listener.join();
}
return 0;
}Potential Pitfalls: Kernel Starvation and sc_pause
A crucial detail of sc_simcontext::crunch() is how it handles idle time. If the simulation reaches a point where m_timed_events is empty and m_runnable is empty, the sc_start() loop will terminate, assuming the simulation is finished.
If your SystemC model is purely reactive and waiting exclusively on async_request_update() from an OS thread, the kernel will see an empty event queue and exit instantly, terminating your process!
To prevent this, you must either:
- Ensure a periodic dummy event (like a clock
SC_THREADthat callswait(1, SC_MS)) keeps the kernel alive. - Use
sc_start()with an explicit maximum time, running the simulation in time slices. - Use the
sc_pause()architecture andsc_start()re-entry if you run the kernel inside another application loop.
Conclusion
The async_request_update() method is your singular bridge between standard C++ multi-threading and the deterministic SystemC execution engine. By strictly guarding shared state with C++ mutexes and deferring event notification to the overridden update() phase, you can safely integrate network interfaces, hardware-in-the-loop (HIL) systems, and interactive GUIs into any SystemC virtual platform.
Comments and Corrections