How Multi-Threading Works with Dear ImGui Contexts: 3 Safe Patterns

Dear ImGui contexts are not thread-safe by default; you must either synchronize every API call with a mutex, stage the resulting ImDrawData for a separate render thread, or compile with thread-local storage to maintain independent contexts per thread.

Dear ImGui (ocornut/imgui) centralizes all per-application state in opaque ImGuiContext structures accessed through the global pointer GImGui. Because this pointer is a plain global variable defined in imgui_internal.h, concurrent access from multiple threads causes data races on critical members like ActiveId, CurrentWindow, and SettingsHandlers. Understanding how multi-threading works with Dear ImGui contexts requires adopting one of three documented patterns that ensure thread safety without modifying the library core.

Understanding the Global Context Pointer

The library exposes the current context through ImGui::GetCurrentContext() and switches contexts via ImGui::SetCurrentContext(), both declared in imgui.h. Internally, these functions manipulate the global pointer GImGui defined near line 227 of imgui_internal.h:

// imgui_internal.h (line 227)
#ifndef GImGui
#define GImGui GImGui
#endif
extern IMGUI_API ImGuiContext* GImGui;  // Current implicit context pointer

All inline helper functions—such as ImGui::GetIO(), ImGui::GetCurrentWindow(), and the entirety of NewFrame()—dereference this pointer without synchronization. Consequently, a single ImGuiContext* must never be accessed simultaneously from multiple threads.

Three Safe Patterns for Multi-Threading

The official FAQ and reference implementations support three distinct strategies for using Dear ImGui in concurrent environments.

1. Mutex-Protected Single Context (Lock-Around-the-Context)

Wrap every ImGui call that touches the same ImGuiContext* with a user-provided std::mutex. This serializes all UI updates and rendering, preventing simultaneous read/write access to the context's internal state.

Use this pattern for simple debug tools or background updates where UI work is brief and the main thread can tolerate blocking. Declare the mutex alongside your context pointer:

std::mutex g_ImGuiMutex;
ImGuiContext* g_ImGuiCtx = nullptr;  // Created once via ImGui::CreateContext()

2. Staging ImDrawData for Render Threads

Build the UI on a producer thread (typically the update loop) and hand the resulting ImDrawData to a consumer thread that executes GPU draw calls. The ImDrawDataSnapshot helper struct (available in the imgui_threaded_rendering example from the ImGui Club repository) creates a thread-safe copy of vertex/index buffers and texture references, allowing the render thread to operate without touching the live context.

This approach fits classic game-loop architectures where the UI updates on the logic thread but submits to a dedicated graphics thread. The snapshot is created after ImGui::Render() and passed across threads via a lock-free queue.

3. Thread-Local Storage with One Context Per Thread

Define GImGui as a thread-local variable so each OS thread maintains an independent context pointer. Add the following to imconfig.h or your build configuration before including Dear ImGui headers:

// For GCC/Clang
#define GImGui __thread ImGuiContext* GImGui

// For MSVC
// #define GImGui __declspec(thread) ImGuiContext* GImGui

Each thread then calls ImGui::CreateContext() to obtain a unique instance, enabling parallel UI rendering for multiple viewports or isolated editor panes without cross-thread synchronization.

Core API Functions and Source Files

The public API for context management resides in imgui.h and imconfig.h (for the TLS override):

// imgui.h
IMGUI_API ImGuiContext* CreateContext(ImFontAtlas* shared_font_atlas = NULL);
IMGUI_API void          DestroyContext(ImGuiContext* ctx = NULL);
IMGUI_API ImGuiContext* GetCurrentContext();
IMGUI_API void          SetCurrentContext(ImGuiContext* ctx);

These functions allocate the ImGuiContext structure (containing Windows, InputTextState, Style, and runtime variables) and bind it to the current thread's GImGui pointer. When using the thread-local pattern, SetCurrentContext() updates only the calling thread's instance, while other threads retain their own isolated contexts.

Practical Implementation Examples

Synchronizing with a Mutex

// ui_thread.cpp
#include "imgui.h"
#include <mutex>
#include <thread>

extern std::mutex g_ImGuiMutex;
extern ImGuiContext* g_ImGuiCtx;

void UIUpdateThread()
{
    std::lock_guard<std::mutex> lock(g_ImGuiMutex);
    ImGui::SetCurrentContext(g_ImGuiCtx);
    ImGui::NewFrame();
    
    ImGui::Begin("Background Tool");
    ImGui::Text("Thread ID: %d", std::this_thread::get_id());
    ImGui::End();
    
    ImGui::Render();
    // Store ImGui::GetDrawData() for the render thread
}

Staging Draw Data for Async Rendering

// Requires imgui_threaded_rendering.h from imgui_club
#include "imgui_threaded_rendering.h"
#include <queue>
#include <mutex>

std::mutex g_QueueMutex;
std::queue<ImDrawDataSnapshot> g_DrawQueue;

void ProducerThread()
{
    ImGui::SetCurrentContext(g_ImGuiCtx);
    ImGui::NewFrame();
    // ... populate UI ...
    ImGui::Render();
    
    std::lock_guard<std::mutex> lock(g_QueueMutex);
    g_DrawQueue.emplace(*ImGui::GetDrawData());
}

void ConsumerRenderThread()
{
    ImDrawDataSnapshot snapshot;
    {
        std::lock_guard<std::mutex> lock(g_QueueMutex);
        if (g_DrawQueue.empty()) return;
        snapshot = std::move(g_DrawQueue.front());
        g_DrawQueue.pop();
    }
    // Submit snapshot.VertexBuffer and IndexBuffer to GPU
    // No ImGui context accessed here
}

Per-Thread Contexts with TLS

// Define before any Dear ImGui includes
#define GImGui __thread ImGuiContext* GImGui
#include "imgui.h"

void WorkerThread()
{
    ImGuiContext* ctx = ImGui::CreateContext();
    ImGui::SetCurrentContext(ctx);
    
    ImGui::NewFrame();
    ImGui::Begin("Isolated Window");
    ImGui::Text("Private context on thread %d", std::this_thread::get_id());
    ImGui::End();
    ImGui::Render();
    
    // Render or transfer ImDrawData...
    ImGui::DestroyContext(ctx);
}

Summary

  • A single ImGuiContext is not thread-safe; concurrent access causes data races on internal state members.
  • Three safe patterns exist: mutex-wrapped access for simple serialization, ImDrawDataSnapshot for producer-consumer architectures, and thread-local GImGui for parallel independent contexts.
  • The global pointer GImGui lives in imgui_internal.h (line 227) and defaults to a plain global, but can be redefined as thread-local via imconfig.h.
  • Always use ImGui::CreateContext() and ImGui::SetCurrentContext() to manage context lifecycles, and consult the imgui_threaded_rendering example for production-ready draw data staging.

Frequently Asked Questions

Can I call ImGui::NewFrame() from one thread and ImGui::Render() from another?

No, you cannot call ImGui::NewFrame() and ImGui::Render() on different threads for the same context without synchronization. These functions both read and modify shared state within the ImGuiContext structure. You must either wrap both calls with the same mutex or use the staging pattern where the producer thread completes Render(), captures the ImDrawData, and passes it to the consumer thread.

How do I safely pass ImDrawData to a render thread without copying?

Use the ImDrawDataSnapshot helper from the imgui_threaded_rendering example in the ImGui Club repository. This struct performs a deep copy of the vertex and index buffers while maintaining texture references, allowing the render thread to consume the data without accessing the live ImGui context. The original context can then begin the next frame immediately while the GPU processes the snapshot.

What is the performance cost of using thread-local storage for GImGui?

The cost is negligible on modern compilers and operating systems—typically a single pointer indirection per ImGui call, identical to the global version. The primary trade-off is memory usage, as each thread maintains a full ImGuiContext instance (including font atlases and window buffers). Use this pattern only when you genuinely need concurrent UI processing across multiple threads, not for simple background loading screens.

Where should I define GImGui as thread-local?

Define GImGui as a thread-local variable in imconfig.h (which is included by imgui.h) or pass it as a compiler flag (-DGImGui=__thread\ ImGuiContext*\ GImGui). This ensures the definition precedes the declaration in imgui_internal.h. Avoid modifying imgui_internal.h directly to preserve compatibility with upstream updates.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →