A template for multithreading? (GUI & Worker) #9539

Closed
opened 2026-09-09 16:58:04 +00:00 by LRDPRDX · 2 comments
LRDPRDX commented 2026-09-09 16:58:04 +00:00 (Migrated from github.com)

Version/Branch of Dear ImGui:

Version (head): 39e7bf5a0, Branch: master

Back-ends:

imgui_impl_sdl2.cpp + imgui_impl_opengl2.cpp

Compiler, OS:

Linux + GCC

Full config/build information:

Dear ImGui 1.92.8 WIP (19272)
--------------------------------
sizeof(size_t): 8, sizeof(ImDrawIdx): 2, sizeof(ImDrawVert): 20
define: __cplusplus=201103
define: __linux__
define: __GNUC__=13
IM_ASSERT: runs expression: OK. expand size: OK
--------------------------------
io.BackendPlatformName: imgui_impl_sdl2 (2.30.0, 2.30.0)
io.BackendRendererName: imgui_impl_opengl2
io.ConfigFlags: 0x00000003
 NavEnableKeyboard
 NavEnableGamepad
io.ConfigNavCaptureKeyboard
io.ConfigInputTextCursorBlink
io.ConfigWindowsResizeFromEdges
io.ConfigMemoryCompactTimer = 60.0
io.BackendFlags: 0x00000016
 HasMouseCursors
 HasSetMousePos
 RendererHasTextures
--------------------------------
io.Fonts: 1 fonts, Flags: 0x00000000, TexSize: 512,128
io.Fonts->FontLoaderName: stb_truetype
io.DisplaySize: 1906.00,1048.00
io.DisplayFramebufferScale: 1.00,1.00
--------------------------------
style.WindowPadding: 8.00,8.00
style.WindowBorderSize: 1.00
style.FramePadding: 4.00,3.00
style.FrameRounding: 0.00
style.FrameBorderSize: 0.00
style.ItemSpacing: 8.00,4.00
style.ItemInnerSpacing: 4.00,4.00

Details:

Dear Community,

First of all sorry for yet another question about multithreading. I have read several (maybe not all) similar (at least at first sight) issues but I didn't find anything which I could find helpful.

My task is simple. I have GUI to control and read some hardware (a microcontroller). Now everything is done in one thread, so one can see in the code:

    if(auto state = t.state(); state == t5::EState_t::eIdle)
    {
        if(axis.mode == Axis::EMode_t::eRelative)
        {
            if(ImGui::Button(axis.icons.left.c_str(), { 0, 0 }))
                t.go(axis.axis, t5::EDir_t::eCW, axis.steps.target);
            ImGui::SameLine();
            if(ImGui::Button(axis.icons.right.c_str(), { 0, 0 }))
                t.go(axis.axis, t5::EDir_t::eCCW, axis.steps.target);
        }
        else
        {
            if(ImGui::Button("Go"))
                t.go2(axis.axis, axis.steps.target);
        }
    }

all of those member-function calls of the object t are actual communications to the microcontroller. Now everything is working fine (though FPS is 20-22) and it doesn't feel freezing or lagging. However, I would like to split somehow the part of reading / writing to the uC and actually displaying the GUI. I don't quite understand what is the design it should be ? More over, I have a long running tasks like caibration. And I want to display the progress, for example. So the only one way that comes to my mind is like having a proxy object (protected by a mutex) (or objects in a queue) which would represent a state of my uC (current positions, moving or idleing, etc) and also a queue of commands (also protected) (which means writing to the uC). Then one thread (the worker) is the only thread which has the actual access to the device, communicates with it according to the command queue, and fills the proxy object (locking the mutex before). Then in the GUI (main) thread we read the proxy object and fill the GUI dynamic widgets (like labels and plots) (also locking the mutex before).

Is this approach natural or I must learn more about the imGui internals to do it ? Because I see people more asking about two different threads like rendering and the rest, but not about ubiquitous "Worker & GUI" case.

Thank you in advance !

Screenshots/Video:

Image

Minimal, Complete and Verifiable Example code:

Don't think I have something for this type of issue.

### Version/Branch of Dear ImGui: Version (head): 39e7bf5a0, Branch: master ### Back-ends: imgui_impl_sdl2.cpp + imgui_impl_opengl2.cpp ### Compiler, OS: Linux + GCC ### Full config/build information: ```cpp Dear ImGui 1.92.8 WIP (19272) -------------------------------- sizeof(size_t): 8, sizeof(ImDrawIdx): 2, sizeof(ImDrawVert): 20 define: __cplusplus=201103 define: __linux__ define: __GNUC__=13 IM_ASSERT: runs expression: OK. expand size: OK -------------------------------- io.BackendPlatformName: imgui_impl_sdl2 (2.30.0, 2.30.0) io.BackendRendererName: imgui_impl_opengl2 io.ConfigFlags: 0x00000003 NavEnableKeyboard NavEnableGamepad io.ConfigNavCaptureKeyboard io.ConfigInputTextCursorBlink io.ConfigWindowsResizeFromEdges io.ConfigMemoryCompactTimer = 60.0 io.BackendFlags: 0x00000016 HasMouseCursors HasSetMousePos RendererHasTextures -------------------------------- io.Fonts: 1 fonts, Flags: 0x00000000, TexSize: 512,128 io.Fonts->FontLoaderName: stb_truetype io.DisplaySize: 1906.00,1048.00 io.DisplayFramebufferScale: 1.00,1.00 -------------------------------- style.WindowPadding: 8.00,8.00 style.WindowBorderSize: 1.00 style.FramePadding: 4.00,3.00 style.FrameRounding: 0.00 style.FrameBorderSize: 0.00 style.ItemSpacing: 8.00,4.00 style.ItemInnerSpacing: 4.00,4.00 ``` ### Details: Dear Community, First of all sorry for yet another question about multithreading. I *have* read several (maybe not all) similar (at least at first sight) issues but I didn't find anything which I could find helpful. My task is simple. I have GUI to control and read some hardware (a microcontroller). Now everything is done in one thread, so one can see in the code: ```cxx if(auto state = t.state(); state == t5::EState_t::eIdle) { if(axis.mode == Axis::EMode_t::eRelative) { if(ImGui::Button(axis.icons.left.c_str(), { 0, 0 })) t.go(axis.axis, t5::EDir_t::eCW, axis.steps.target); ImGui::SameLine(); if(ImGui::Button(axis.icons.right.c_str(), { 0, 0 })) t.go(axis.axis, t5::EDir_t::eCCW, axis.steps.target); } else { if(ImGui::Button("Go")) t.go2(axis.axis, axis.steps.target); } } ``` all of those member-function calls of the object `t` are actual communications to the microcontroller. Now everything is working fine (though FPS is 20-22) and it doesn't feel freezing or lagging. However, I would like to split somehow the part of reading / writing to the uC and actually displaying the GUI. I don't quite understand what is the design it should be ? More over, I have a long running tasks like caibration. And I want to display the progress, for example. So the only one way that comes to my mind is like having a proxy object (protected by a mutex) (or objects in a queue) which would represent a state of my uC (current positions, moving or idleing, etc) and also a queue of commands (also protected) (which means writing to the uC). Then one thread (the worker) is the only thread which has the actual access to the device, communicates with it according to the command queue, and fills the proxy object (locking the mutex before). Then in the GUI (main) thread we read the proxy object and fill the GUI dynamic widgets (like labels and plots) (also locking the mutex before). Is this approach natural or I must learn more about the imGui internals to do it ? Because I see people more asking about two different threads like rendering and the rest, but not about ubiquitous "Worker & GUI" case. Thank you in advance ! ### Screenshots/Video: <img width="1898" height="1043" alt="Image" src="https://github.com/user-attachments/assets/92196f2b-ad6a-4f07-bd22-dd1a269bb691" /> ### Minimal, Complete and Verifiable Example code: Don't think I have something for this type of issue.
GamingMinds-DanielC commented 2026-09-10 10:05:30 +00:00 (Migrated from github.com)

That (UI/ImGui in main thread, device communication in a worker thread, synchronization through a proxy object) is a natural approach. From the perspective of the UI, there is no real difference to a single threaded approach, so you don't have to worry about ImGui internals or specifics for this.

To keep this smooth, you should minimize the time spend in a locked mutex in both threads. In the main thread, don't lock the mutex and construct your UI based on the state of the proxy object. Instead, lock the mutex once per frame, copy out the state, append the commands collected during the last frame to the command queue, then unlock the mutex right away and construct the UI based on your copy of the state. In the worker thread, don't lock the mutex and update the state by polling your device. Poll your device first, update or construct a state local to the thread, then once per cycle lock the mutex and copy into the proxy, collect new commands for the next cycle while at it and unlock again.

If you do this carefully, you can probably even avoid any allocations while in a locked mutex. If your command queue is f.e. an std::list, you can splice its contents to the end of another list without allocating or freeing anything. If you construct a complete state per cycle in your worker thread, you can swap it with the proxy state instead of copying. That way you can avoid expensive memory operations inside of the mutex if you have dynamic containers as part of the state. You can also swap the state in your main thread instead of copying it once per frame, but then you need to secure it with a generation (number incremented on every update, only swap out if number changed) or a simple flag (set to valid when swapping in a new state, only swap out if valid, set to invalid after swapping out).

You should always be aware of the possibility of multiple update cycles happening between two frames or multiple frames being displayed between two update cycles. Do not assume that the commands appended to the queue one frame earlier have been delivered to the worker thread yet. Also, if your state contains deltas of some kind, don't assume the UI thread has seen them yet when updating your state again.

That (UI/ImGui in main thread, device communication in a worker thread, synchronization through a proxy object) is a natural approach. From the perspective of the UI, there is no real difference to a single threaded approach, so you don't have to worry about ImGui internals or specifics for this. To keep this smooth, you should minimize the time spend in a locked mutex in both threads. In the main thread, don't lock the mutex and construct your UI based on the state of the proxy object. Instead, lock the mutex once per frame, copy out the state, append the commands collected during the last frame to the command queue, then unlock the mutex right away and construct the UI based on your copy of the state. In the worker thread, don't lock the mutex and update the state by polling your device. Poll your device first, update or construct a state local to the thread, then once per cycle lock the mutex and copy into the proxy, collect new commands for the next cycle while at it and unlock again. If you do this carefully, you can probably even avoid any allocations while in a locked mutex. If your command queue is f.e. an `std::list`, you can splice its contents to the end of another list without allocating or freeing anything. If you construct a complete state per cycle in your worker thread, you can swap it with the proxy state instead of copying. That way you can avoid expensive memory operations inside of the mutex if you have dynamic containers as part of the state. You can also swap the state in your main thread instead of copying it once per frame, but then you need to secure it with a generation (number incremented on every update, only swap out if number changed) or a simple flag (set to valid when swapping in a new state, only swap out if valid, set to invalid after swapping out). You should always be aware of the possibility of multiple update cycles happening between two frames or multiple frames being displayed between two update cycles. Do not assume that the commands appended to the queue one frame earlier have been delivered to the worker thread yet. Also, if your state contains deltas of some kind, don't assume the UI thread has seen them yet when updating your state again.
LRDPRDX commented 2026-09-10 12:56:45 +00:00 (Migrated from github.com)

To keep this smooth, you should minimize the time spend in a locked mutex in both threads. In the main thread, don't lock the mutex and construct your UI based on the state of the proxy object. Instead, lock the mutex once per frame, copy out the state, append the commands collected during the last frame to the command queue, then unlock the mutex right away and construct the UI based on your copy of the state. In the worker thread, don't lock the mutex and update the state by polling your device. Poll your device first, update or construct a state local to the thread, then once per cycle lock the mutex and copy into the proxy, collect new commands for the next cycle while at it and unlock again.

Yes, this is exactly how it was in my mind :)

You should always be aware of the possibility of multiple update cycles happening between two frames or multiple frames being displayed between two update cycles.

Yes, of course.

Do not assume that the commands appended to the queue one frame earlier have been delivered to the worker thread yet

Yes, this is on of the most important points I was thinking about.

Also, if your state contains deltas of some kind, don't assume the UI thread has seen them yet when updating your state again.

This is also important indeed: if, f.e. the uC is not in the idle state you cannot move (so the button must turn from "Go" into "Stop").

Okay, I will start working on it on Saturday as I am quite busy now. Thank you for your help !

> To keep this smooth, you should minimize the time spend in a locked mutex in both threads. In the main thread, don't lock the mutex and construct your UI based on the state of the proxy object. Instead, lock the mutex once per frame, copy out the state, append the commands collected during the last frame to the command queue, then unlock the mutex right away and construct the UI based on your copy of the state. In the worker thread, don't lock the mutex and update the state by polling your device. Poll your device first, update or construct a state local to the thread, then once per cycle lock the mutex and copy into the proxy, collect new commands for the next cycle while at it and unlock again. Yes, this is exactly how it was in my mind :) > You should always be aware of the possibility of multiple update cycles happening between two frames or multiple frames being displayed between two update cycles. Yes, of course. > Do not assume that the commands appended to the queue one frame earlier have been delivered to the worker thread yet Yes, this is on of the most important points I was thinking about. > Also, if your state contains deltas of some kind, don't assume the UI thread has seen them yet when updating your state again. This is also important indeed: if, f.e. the uC is not in the `idle` state you cannot move (so the button must turn from "Go" into "Stop"). Okay, I will start working on it on Saturday as I am quite busy now. Thank you for your help !
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ocornut/imgui#9539
No description provided.