| name | async-threading |
| description | Threading model and async operation patterns for Windows applications. Use when implementing background operations, thread synchronization, posting to UI thread, or using thread pools. |
| metadata | {"author":"RedSalamander","version":"1.0"} |
Threading and Async Operations
Threading Model
- UI operations → Main thread only
- Heavy computations → Worker threads
- Never block UI thread with synchronous waits
Synchronization with WIL
wil::srwlock _lock;
void WriteOperation()
{
auto guard = _lock.lock_exclusive();
}
void ReadOperation()
{
auto guard = _lock.lock_shared();
}
Event Signaling
wil::unique_event _completionEvent;
_completionEvent.create();
_completionEvent.SetEvent();
if (_completionEvent.wait(100) == WAIT_OBJECT_0)
{
}
Post to UI Thread
constexpr UINT WM_APP_WORK = WM_APP + 1;
struct UiWorkItem
{
int kind;
std::wstring text;
};
void PostToUIThread(UiWorkItem item)
{
auto payload = std::make_unique<UiWorkItem>(std::move(item));
static_cast<void>(PostMessagePayload(_hWnd, WM_APP_WORK, 0, std::move(payload)));
}
case WM_APP_WORK:
{
auto payload = TakeMessagePayload<UiWorkItem>(lParam);
if (! payload)
{
return 0;
}
return 0;
} break;
HWND Teardown and Posted Payloads
If an HWND can be destroyed while payload messages are still queued, Windows may discard those messages without delivering them. To prevent payload leaks (and to keep the PostMessagePayload registry consistent):
- Call
InitPostedPayloadWindow(hwnd) during create (WM_NCCREATE/WM_CREATE).
- Call
DrainPostedPayloadsForWindow(hwnd) in WM_NCDESTROY.
- Always receive with
TakeMessagePayload<T>(lParam) (not a manual unique_ptr wrap).
UI-host cross-thread checklist
For every UI-host change that posts work across threads, review and cover:
- Payload ownership: every heap payload is posted with
PostMessagePayload(...) and received with TakeMessagePayload<T>(lParam).
- UI-thread boundary: UI state is mutated only from the owning UI thread or explicitly marshaled there.
- Cancellation path: producers stop or observe cancellation before teardown begins.
- Teardown drain: receiving windows initialize/drain the posted-payload registry.
- Regression guard: add a focused teardown stress/selftest when the touched host can queue payloads.
Async Icon Pattern
constexpr UINT WM_APP_ICON_READY = WM_APP + 2;
struct IconReadyPayload
{
int idx = 0;
wil::unique_hicon icon;
};
struct IconWorkItem
{
HWND hwnd = nullptr;
int idx = 0;
};
void RequestIconAsync(int idx)
{
const HWND hwnd = _hWnd;
auto ctx = std::make_unique<IconWorkItem>();
ctx->hwnd = hwnd;
ctx->idx = idx;
const BOOL queued = TrySubmitThreadpoolCallback(
[](PTP_CALLBACK_INSTANCE , void* context) noexcept
{
std::unique_ptr<IconWorkItem> ctx(static_cast<IconWorkItem*>(context));
if (! ctx || ! ctx->hwnd)
{
return;
}
[[maybe_unused]] auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED);
wil::unique_hicon icon = IconCache::GetInstance().ExtractSystemIcon(ctx->idx);
auto payload = std::make_unique<IconReadyPayload>();
payload->idx = ctx->idx;
payload->icon = std::move(icon);
static_cast<void>(PostMessagePayload(ctx->hwnd, WM_APP_ICON_READY, 0, std::move(payload)));
},
ctx.get(),
nullptr);
if (queued != 0)
{
ctx.release();
}
}
case WM_APP_ICON_READY:
{
auto payload = TakeMessagePayload<IconReadyPayload>(lParam);
if (! payload)
{
return 0;
}
const int idx = payload->idx;
auto bitmap = IconCache::ConvertIconToBitmapOnUIThread(std::move(payload->icon));
return 0;
} break;
Plugin Unload Safety (DLLs)
When scheduling background work from a plugin DLL (viewers/file systems):
- Avoid
std::thread(...).detach().
- Prefer
TrySubmitThreadpoolCallback or std::jthread tracked on the instance and stopped/joined on unload.
- If a callback can outlive the host’s last reference to the DLL, pin the module for the callback lifetime:
- add a static anchor in the module:
static const int kMyPluginModuleAnchor = 0;
- store
wil::unique_hmodule moduleKeepAlive = AcquireModuleReferenceFromAddress(&kMyPluginModuleAnchor); in the callback context
- touch it in the callback:
static_cast<void>(ctx->moduleKeepAlive);
Best Practices
- Use message queues for cross-thread communication
- Prefer lock-free patterns where possible
- Handle thread cancellation gracefully
- Profile multi-threaded code paths
- Implement cleanup/cancel mechanisms
catch (...) is FORBIDDEN; if catching is mandatory at a thread/callback boundary, catch only explicitly named exception types and add a short comment explaining why.
Resource Management
Caching
- Implement LRU caches for expensive resources
- Use weak references to break cycles
- Monitor resource usage and implement limits
Thread Pools
wil::unique_any<PTP_WORK, decltype(&::CloseThreadpoolWork), ::CloseThreadpoolWork> _work;
void StartWork(void* context)
{
_work.reset(CreateThreadpoolWork(&WorkCallback, context, nullptr));
SubmitThreadpoolWork(_work.get());
}
void CancelWork()
{
if (_work)
{
WaitForThreadpoolWorkCallbacks(_work.get(), TRUE);
_work.reset();
}
}
Architecture Patterns
Component Design
- Single responsibility principle
- Composition over inheritance
- Proper separation of concerns
- Design for testability
Device Loss Handling
- Implement proper D2D device loss handling
- Cache expensive resources with LRU eviction
- Use weak references to break cycles