When engineering high-frequency trading (HFT/HTS) systems or mission-critical C++ applications where zero runtime exceptions are permitted, understanding Inter-Process Communication (IPC) and OS-level memory lifetime management is crucial.
This post covers the complete architectural spectrum: common memory pitfalls in WM_COPYDATA, high-throughput Zero-Copy Shared Memory (File Mapping) internals, and nanosecond-latency Lock-Free (SPSC/MPMC) Ring Buffers with C++11 std::atomic memory ordering.
1. WM_COPYDATA Internals & Why Receivers Must NEVER Call Delete
A common mistake when sending data via WM_COPYDATA from Process A to Process B is assuming Process B should call delete once finished. Doing so results in immediate Access Violation crashes.
- Independent Virtual Address Spaces: Every Windows process resides in its own isolated virtual address space. Process A's heap and Process B's heap are completely separate.
- Heap Manager Conflict: Memory allocated via
newin Process A is tracked exclusively by Process A's heap manager. When Process B attempts todeletethis pointer, Process B's heap manager detects an unknown pointer and aborts the process instantly.
WM_COPYDATA Kernel Lifecycle
- SendMessage Transmission: Process A populates
COPYDATASTRUCTand invokesSendMessage. (AsynchronousPostMessageis strictly forbidden as the OS cannot guarantee memory lifetime). - Kernel Mapping: The Windows Kernel maps/copies Process A's memory into Process B's virtual address space temporarily.
- Handler Return & Auto-Unmapping: As soon as Process B's
WM_COPYDATAhandler returns, the Windows kernel invalidates the temporary mapping in Process B instantly.
If Process B needs to process the received payload asynchronously in a queue or worker thread, Process B must allocate its own heap memory and perform a Deep Copy before the message handler returns.
2. WM_COPYDATA vs Shared Memory (File Mapping)
For high-frequency market tick feeds or high-throughput data streams, developers should use Shared Memory (File Mapping) instead of WM_COPYDATA to eliminate copying overhead.
π IPC Paradigm Comparison
- WM_COPYDATA: Kernel memory copy occurs, slower, auto-synchronized via SendMessage, destroyed on handler return
- Shared Memory: Zero-Copy (shared physical RAM), extremely fast, manual synchronization (Mutex/Event/Lock-Free), lives until ref-count hits 0
Common Misconception: "Is Shared Memory Stored on Hard Disk?"
Passing INVALID_HANDLE_VALUE as the first parameter to CreateFileMapping backs the mapping via the OS Pagefile, operating entirely within high-speed physical RAM. Unless system RAM is exhausted forcing swap-out, Disk I/O is 0 Bytes.
Shared memory is managed via kernel object reference counts. When Process A calls
CloseHandle, the shared memory persists if Process B is still attached. The OS reclaims physical memory only when all processes unmap and close handles, dropping the reference count to 0.
3. Lock-Free SPSC (Single-Producer Single-Consumer) Ring Buffer
Kernel synchronization objects like Mutex or Event introduce User β Kernel mode context switching overhead. Ultra-low latency systems rely on User-Mode Lock-Free Ring Buffers using atomic CPU instructions.
SPSC Ring Buffer & Memory Ordering (C++11)
// Shared Memory Control Block Layout
struct ControlBlock {
std::atomic<size_t> head{0}; // Updated by Consumer
uint8_t padding1[56]; // 64-byte Cache Line Padding (Prevents False Sharing)
std::atomic<size_t> tail{0}; // Updated by Producer
uint8_t padding2[56];
};
// 1. Producer Logic
void Produce(const DataPacket& packet) {
size_t current_tail = control->tail.load(std::memory_order_relaxed);
ring_buffer[current_tail] = packet;
// memory_order_release guarantees buffer writes complete before updating Tail
size_t next_tail = (current_tail + 1) % RING_BUFFER_SIZE;
control->tail.store(next_tail, std::memory_order_release);
}
// 2. Consumer Logic
bool Consume(DataPacket& outPacket) {
// memory_order_acquire prevents reads from being reordered prior to Tail load
size_t current_tail = control->tail.load(std::memory_order_acquire);
size_t current_head = control->head.load(std::memory_order_relaxed);
if (current_head == current_tail) {
return false; // Queue empty
}
outPacket = ring_buffer[current_head];
size_t next_head = (current_head + 1) % RING_BUFFER_SIZE;
control->head.store(next_head, std::memory_order_release);
return true;
}
If Head and Tail variables reside within the same 64-byte CPU cache line, CPU cores invalidate each other's cache constantly. Inserting
uint8_t padding[56] separates Head and Tail into distinct cache lines, unlocking maximum throughput.
4. MPMC Lock-Free, CAS (Compare-And-Swap) & The ABA Problem
In Multi-Producer Multi-Consumer (MPMC) architectures, multiple processes compete to claim indices using CAS (Compare-And-Swap).
// Producer Enqueue CAS Loop (C++11)
void MPMC_Enqueue(const DataPacket& data) {
size_t currentTail = tail.load(std::memory_order_relaxed);
size_t nextTail;
while (true) {
nextTail = (currentTail + 1) % QUEUE_SIZE;
// Atomic CAS Attempt
if (tail.compare_exchange_weak(currentTail, nextTail,
std::memory_order_release,
std::memory_order_relaxed)) {
// Success: currentTail slot claimed exclusively!
break;
}
// Failure: Another process updated tail. Retry loop with updated currentTail
}
slots[currentTail].data = data;
slots[currentTail].status.store(SLOT_READY, std::memory_order_release);
}
The ABA Problem & Its Solution
If Process 1 reads state A, gets preempted, and another process transitions the state A β B β A, Process 1's CAS succeeds incorrectly despite corrupted underlying data structures.
Instead of comparing index values alone, pack a 64-bit monotonically increasing **Sequence Number** into the atomic variable. Even if the value reverts to A, the altered sequence number causes CAS to detect changes and fail correctly.
5. Summary
- Receiving processes in
WM_COPYDATAmust never calldeleteand must perform a Deep Copy inside the handler. - High-throughput IPC uses
CreateFileMappingZero-Copy Shared Memory, managed via kernel reference counts. - Ultra-low latency systems bypass Mutex overhead by combining SPSC/MPMC Lock-Free Ring Buffers, C++11 atomic memory ordering, CAS, and cache-line padding.