%0
100%
C

Mutex, Shared Memory Inter-Process Communication Methods in Windows

OsderdaDev
August 10, 2026
25 views

Operating systems enforce virtual memory isolation to prevent one application from corrupting or inspecting the memory of another. While this boundary is essential for stability and security, modern software architectures frequently require independent processes to exchange data, coordinate tasks, or share real-time state. I've addressed the necessary issues.

In is it Windows user-mode, Inter-Process Communication (IPC) provides several mechanisms to cross this process boundary. Choosing the right technique depends on bandwidth requirements, latency tolerance, complex synchronization needs, and whether the processes share a parent-child relationship. It's essential.

1. Shared Memory via Memory-Mapped Files

Shared memory; is the fastest form of IPC available on Windows because data does not need to be copied through the kernel for every transfer. Once the shared memory region is mapped into the virtual address space of both processes, writes by one process are immediately visible to the other at hardware memory speeds.

Under the hood, Windows implements shared memory using Pagefile-Backed Memory-Mapped Files. Instead of mapping a physical file on disk, you pass INVALID_HANDLE_VALUE as the file handle to CreateFileMappingW. The OS allocates pages from the system paging file and assigns a global name to the mapping object.

Creating and Writing to Shared Memory

The producing process creates the named mapping and maps a view of it into its virtual address space:

cpp
#include <windows.h>
#include <iostream>

struct SharedPayload {
    DWORD counter;
    wchar_t message[256];
};//struct cpp

int main() {
    const wchar_t* mappingName = L"Local\\MyCustomSharedMemory";
    SIZE_T bufferSize = sizeof(SharedPayload);// sixeof SharedPayloaf

    // Create a pagefile-backed file mapping object
    HANDLE hMapFile = CreateFileMappingW(
        INVALID_HANDLE_VALUE,    // Backed by system paging file
        NULL,                    // Default security attributes
        PAGE_READWRITE,          // Read/write access
        0,                       // High-order DWORD of size
        static_cast<DWORD>(bufferSize), // Low-order DWORD of size
        mappingName              // Name of the mapping object
    );

    if (hMapFile == NULL) {
        std::cerr << "CreateFileMappingW failed: " << GetLastError() << '\n';
        return 1;
    }

    // Map a view of the memory into the process address space
    SharedPayload* pPayload = static_cast<SharedPayload*>(
        MapViewOfFile(
            hMapFile,
            FILE_MAP_ALL_ACCESS,
            0, 0, bufferSize
        )
    );

    if (pPayload == NULL) {
        std::cerr << "MapViewOfFile failed: " << GetLastError() << '\n';
        CloseHandle(hMapFile);
        return 1;
    }

    // Write data directly to the shared memory view
    pPayload->counter = 1001;
    wcscpy_s(pPayload->message, L"Hello from Producer Process!");

    std::cout << "Data written to shared memory. Press ENTER to release...\n";
    std::cin.get();

    // Cleanup
    UnmapViewOfFile(pPayload);
    CloseHandle(hMapFile);
    return 0;
}

Reading from Shared Memory

The consuming process uses OpenFileMappingW with the same global name to map the existing region:

cpp
#include <windows.h>
#include <iostream>

struct SharedPayload {
    DWORD counter;
    wchar_t message[256];
};

int main() {
    const wchar_t* mappingName = L"Local\\MyCustomSharedMemory";

    // Open the existing named memory mapping
    HANDLE hMapFile = OpenFileMappingW(
        FILE_MAP_READ,   // Read-only access
        FALSE,           // Do not inherit handle
        mappingName
    );

    if (hMapFile == NULL) {
        std::cerr << "OpenFileMappingW failed: " << GetLastError() << '\n';
        return 1;
    }

    SharedPayload* pPayload = static_cast<SharedPayload*>(
        MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, sizeof(SharedPayload))
    );

    if (pPayload == NULL) {
        std::cerr << "MapViewOfFile failed: " << GetLastError() << '\n';
        CloseHandle(hMapFile);
        return 1;
    }

    std::wcout << L"Counter: " << pPayload->counter << L"\n";
    std::wcout << L"Message: " << pPayload->message << L"\n";

    UnmapViewOfFile(pPayload);
    CloseHandle(hMapFile);
    return 0;
}

2. Synchronizing Shared Memory Access with Named Mutexes

Because shared memory allows concurrent access from multiple virtual address spaces, access to the shared buffer is inherently prone to race conditions. If one process writes to the buffer while another is reading, memory corruption or torn reads will occur.

To synchronize access across different processes, Windows provides Kernel Named Synchronization Primitives (Mutexes, Semaphores, and Events). Unlike standard std::mutex which operates only within a single process, a named kernel Mutex can be acquired by any process that knows its string name.

cpp
#include <windows.h>
#include <iostream>

class ScopedNamedMutex {
private:
    HANDLE m_hMutex;
    bool m_acquired;

public:
    ScopedNamedMutex(const wchar_t* name) : m_acquired(false) {
        // Create or open a named kernel mutex
        m_hMutex = CreateMutexW(NULL, FALSE, name);
        if (m_hMutex != NULL) {
            // Wait up to 5 seconds to acquire ownership
            DWORD result = WaitForSingleObject(m_hMutex, 5000);
            if (result == WAIT_OBJECT_0) {
                m_acquired = true;
            }
        }
    }

    bool isAcquired() const { return m_acquired; }

    ~ScopedNamedMutex() {
        if (m_acquired) {
            ReleaseMutex(m_hMutex);
        }
        if (m_hMutex != NULL) {
            CloseHandle(m_hMutex);
        }
    }
};

// Usage in Producer/Consumer:
void SafelyWriteData() {
    ScopedNamedMutex lock(L"Local\\MySharedMemoryMutex");
    if (lock.isAcquired()) {
        // Perform shared memory writes here safely
    } else {
        std::cerr << "Timed out waiting for IPC Mutex.\n";
    }
}

3. Stream-Based IPC: Named Pipes

While shared memory excels at high-speed data access, it requires manual memory layout and custom synchronization protocols. When your application requires a structured, stream-based, or message-oriented communication channel, Named Pipes are the standard mechanism.

Named Pipes operate in a client-server model. They support half-duplex or full-duplex communication, automatic blocking/non-blocking I/O, and built-in security descriptors to restrict access across user accounts.

Creating a Named Pipe Server

cpp
#include <windows.h>
#include <iostream>

void RunPipeServer() {
    const wchar_t* pipeName = L"\\\\.\\pipe\\MyCustomPipe";

    HANDLE hPipe = CreateNamedPipeW(
        pipeName,
        PIPE_ACCESS_DUPLEX,       // Read/Write access
        PIPE_TYPE_MESSAGE |        // Message-type pipe
        PIPE_READMODE_MESSAGE |    // Message-read mode
        PIPE_WAIT,                 // Blocking mode
        1,                         // Max instances
        1024,                      // Output buffer size
        1024,                      // Input buffer size
        0,                         // Default timeout
        NULL                       // Default security
    );

    if (hPipe == INVALID_HANDLE_VALUE) {
        std::cerr << "CreateNamedPipeW failed: " << GetLastError() << '\n';
        return;
    }

    std::cout << "Waiting for client connection...\n";
    if (ConnectNamedPipe(hPipe, NULL) || GetLastError() == ERROR_PIPE_CONNECTED) {
        const char* response = "ACK: Request Processed";
        DWORD bytesWritten = 0;
        WriteFile(hPipe, response, static_cast<DWORD>(strlen(response)), &bytesWritten, NULL);
    }

    CloseHandle(hPipe);
}

Named Pipe Client

A client application connects to a pipe as if it were a standard file on disk using CreateFileW:

cpp
#include <windows.h>
#include <iostream>

void RunPipeClient() {
    const wchar_t* pipeName = L"\\\\.\\pipe\\MyCustomPipe";

    HANDLE hPipe = CreateFileW(
        pipeName,
        GENERIC_READ | GENERIC_WRITE,
        0, NULL, OPEN_EXISTING, 0, NULL
    );

    if (hPipe != INVALID_HANDLE_VALUE) {
        char buffer[128] = {0};
        DWORD bytesRead = 0;
        if (ReadFile(hPipe, buffer, sizeof(buffer) - 1, &bytesRead, NULL)) {
            std::cout << "Server Response: " << buffer << '\n';
        }
        CloseHandle(hPipe);
    }
}

4. Lightweight Message Passing: WM_COPYDATA

For desktop applications with Win32 window handles (HWND), Windows offers a lightweight, synchronous message-passing mechanism via the WM_COPYDATA window message.

WM_COPYDATA allows an application to pass a pointer to a COPYDATASTRUCT containing payload data. The Windows kernel intercepts the message, allocates temporary memory in the target process's space, copies the payload, and passes the updated pointer to the destination window procedure.

cpp
#include <windows.h>

// Sender Process
void SendDataToWindow(HWND hTargetWnd, DWORD dwTag, const void* pData, DWORD dataSize) {
    COPYDATASTRUCT cds;
    cds.dwData = dwTag;            // Custom identifier tag
    cds.cbData = dataSize;         // Size of data in bytes
    cds.lpData = const_cast<void*>(pData); // Pointer to data

    // WM_COPYDATA MUST be sent via SendMessage (synchronous).
    // PostMessage will fail because the buffer lifetime is tied to the call.
    SendMessageW(hTargetWnd, WM_COPYDATA, reinterpret_cast<WPARAM>(NULL), reinterpret_cast<LPARAM>(&cds));
}

// Receiver Window Procedure (Inside target app's WndProc)
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    if (message == WM_COPYDATA) {
        PCOPYDATASTRUCT pCds = reinterpret_cast<PCOPYDATASTRUCT>(lParam);
        DWORD tag = static_cast<DWORD>(pCds->dwData);
        const char* payload = static_cast<const char*>(pCds->lpData);
        DWORD payloadSize = pCds->cbData;

        // Process the data synchronously. The memory is freed immediately upon return.
        return TRUE;
    }
    return DefWindowProcW(hWnd, message, wParam, lParam);
}

5. Architectural Comparison and Trade-Offs

Selecting the appropriate user-mode IPC mechanism requires balancing throughput, architecture constraints, and implementation complexity:

* Shared Memory: Best for streaming large buffers, video frames, or high-frequency shared state (e.g., telemetry, shared game engine state). Requires explicit synchronization via named mutexes or events.

* Named Pipes: Best for client-server architectures, RPC-like streaming, and structured command handling between background services and desktop apps.

* WM_COPYDATA: Best for simple, low-frequency synchronous payloads between windowed GUI applications (e.g., passing command-line parameters to an already-running single-instance application).

* Loopback Sockets (TCP/UDP): Best when cross-platform compatibility is required or when the client and server may eventually be split across different physical machines on a network.

Finally is communication

Now you are knowledgeable about the necessary topics. How you proceed from here is up to you. Everything has its advantages and disadvantages.

İnteraktif JavaScript Scratchpad Sandbox
Konsol Çıktısı
Run code to view output...

Fetching engineering questions...

Loading recommendation engine...

Osderda

Senior Software Engineer

With a diverse background in Windows system programming, scalable Backend infrastructure, native Android development, and server management, I strive to bridge the gap between different technology stacks. My goal is to simplify complex coding concepts and share actionable knowledge through concise, technical documentation.

Discussion (0)