ktsu.Invoker
1.5.1
Prefix Reserved
See the version list below for details.
dotnet add package ktsu.Invoker --version 1.5.1
NuGet\Install-Package ktsu.Invoker -Version 1.5.1
<PackageReference Include="ktsu.Invoker" Version="1.5.1" />
<PackageVersion Include="ktsu.Invoker" Version="1.5.1" />
<PackageReference Include="ktsu.Invoker" />
paket add ktsu.Invoker --version 1.5.1
#r "nuget: ktsu.Invoker, 1.5.1"
#:package ktsu.Invoker@1.5.1
#addin nuget:?package=ktsu.Invoker&version=1.5.1
#tool nuget:?package=ktsu.Invoker&version=1.5.1
ktsu.Invoker
A .NET library that ensures delegates are executed on the intended thread, simplifying thread management in UI and graphics applications.
Introduction
Invoker is a .NET library that provides methods to ensure delegates are executed on the intended thread. It is designed to simplify task execution and thread management in .NET applications where delegates are required to run within a specific context, such as the UI thread in WPF or WinForms applications, or the window thread in OpenGL or DirectX applications.
Features
- Thread-Safe Execution: Ensure delegates run on the intended thread
- Synchronous & Asynchronous Support: Both blocking and non-blocking invocation patterns
- Return Value Support: Easily retrieve results from cross-thread operations
- Immediate Execution: Auto-detect if already on the target thread for optimal performance
- Queue Management: Built-in task queue with controlled execution timing
- Thread Ownership: Clear ownership model for execution contexts
- Exception Propagation:
Invoke/InvokeAsyncrethrow a delegate's exception to its own caller; a throwingTryBeginInvokeaction is reported through theBeginInvokeFailedevent (or anAggregateExceptionfromDoInvokeswhen nothing handles it) without stopping the rest of the queue, whichever thread calledTryBeginInvoke - Lightweight Design: Minimal overhead for performance-critical applications
Installation
Package Manager Console
Install-Package ktsu.Invoker
.NET CLI
dotnet add package ktsu.Invoker
Package Reference
<PackageReference Include="ktsu.Invoker" Version="x.y.z" />
Usage Examples
Basic Example
// Initialize an instance on the owning thread
var invoker = new Invoker();
// Queue a task from a different thread, which blocks until the delegate has been executed via DoInvokes() on the owning thread
invoker.Invoke(() => Console.WriteLine("Hello, World!"));
// Call DoInvokes() on the owning thread to execute queued tasks
invoker.DoInvokes();
// NOTE: If you queue from the owning thread the delegate will be executed immediately, bypassing DoInvokes()
// Delegates with return values are supported
string result = invoker.Invoke(() => "Hello, World!");
WPF UI Thread Example
using System.Windows;
using ktsu.Invoker;
public partial class MainWindow : Window
{
private readonly Invoker _invoker = new Invoker();
public MainWindow()
{
InitializeComponent();
// Start a background operation
Task.Run(() => BackgroundOperation());
}
private async Task BackgroundOperation()
{
// Simulate work
await Task.Delay(1000);
// Update UI from background thread safely
_invoker.Invoke(() => {
StatusTextBlock.Text = "Operation Completed!";
ResultListBox.Items.Add("Background task result");
});
}
// Call this in your UI event loop or dispatcher
private void ProcessEvents()
{
_invoker.DoInvokes();
}
}
Asynchronous Invocation
using ktsu.Invoker;
// Create invoker on the main thread
var invoker = new Invoker();
// From background thread
Task background = Task.Run(async () => {
// Queue task and continue without waiting. TryBeginInvoke returns false, and drops the
// action, if the bounded queue is full.
if (!invoker.TryBeginInvoke(() => Console.WriteLine("Processing in the background")))
{
Console.WriteLine("Queue full, message dropped");
}
// Queue task and get Task for completion
Task<string> resultTask = invoker.InvokeAsync(() => "Result from main thread");
// Await the result
string result = await resultTask;
Console.WriteLine($"Got result: {result}");
});
// On main thread, keep executing pending operations until the background work is done.
// Awaiting the background task first would hang: its InvokeAsync waits for a DoInvokes call.
while (!background.IsCompleted)
{
invoker.DoInvokes();
}
Game Loop Integration
using ktsu.Invoker;
public class Game
{
private readonly Invoker _invoker = new Invoker();
public void Run()
{
// Start rendering on the main thread
while (true)
{
// Execute any queued operations from other threads
_invoker.DoInvokes();
// Perform rendering
Render();
// Process events, etc.
}
}
private void Render()
{
// OpenGL/DirectX rendering code here
}
// Call this from other threads
public void QueueTextureLoad(string texturePath)
{
// OpenGL/DirectX resources often need to be created on the main thread.
// TryBeginInvoke returns false, and drops the action, if the bounded queue is full.
if (!_invoker.TryBeginInvoke(() => LoadTextureOnMainThread(texturePath)))
{
// Retry next frame, or fall back to the awaitable InvokeAsync
}
}
private void LoadTextureOnMainThread(string texturePath)
{
// Load texture using OpenGL/DirectX APIs
}
}
API Reference
Invoker Class
The main class that manages execution of delegates on the intended thread.
Methods
| Name | Parameters | Return Type | Description |
|---|---|---|---|
Invoke<T> |
Func<T> func |
T |
Executes the function on the owner thread and returns its result, blocking if called from another thread |
Invoke |
Action action |
void |
Executes the action on the owner thread, blocking if called from another thread |
Invoke<T> |
Func<Task<T>> func |
T |
Starts the async function on the owner thread and blocks until its task completes, returning the task's result |
Invoke |
Func<Task> func |
void |
Starts the async function on the owner thread and blocks until its task completes |
InvokeValueTask<T> |
Func<ValueTask<T>> func |
T |
Starts the ValueTask-returning function on the owner thread and blocks until it completes, returning its result |
InvokeValueTask |
Func<ValueTask> func |
void |
Starts the ValueTask-returning function on the owner thread and blocks until it completes |
TryBeginInvoke |
Action action |
bool |
Queues an action to be executed on the owner thread without waiting for completion. Returns false, and drops the action, if the bounded queue is full. From the owner thread the action runs immediately |
InvokeAsync<T> |
Func<T> func |
Task<T> |
Queues a function to be executed on the owner thread and returns a Task that completes with the result |
InvokeAsync<T> |
Func<Task<T>> func |
Task<T> |
Queues an async function to start on the owner thread and returns a Task that completes with the result of the function's task, once that task has completed |
InvokeAsync |
Func<Task> func |
Task |
Queues an async function to start on the owner thread and returns a Task that completes when the function's task does, carrying any exception it throws |
InvokeValueTaskAsync<T> |
Func<ValueTask<T>> func |
Task<T> |
Queues a ValueTask<T>-returning function to start on the owner thread and returns a Task that completes with its result, once it has completed |
InvokeValueTaskAsync |
Func<ValueTask> func |
Task |
Queues a ValueTask-returning function to start on the owner thread and returns a Task that completes when it does, carrying any exception it throws |
InvokeAsync |
Action action |
Task |
Queues an action to be executed on the owner thread and returns a Task that completes when the action is done |
DoInvokes |
void |
Processes the invocations pending when it is called (must be called from the owner thread). Work queued while it runs waits for the next call; see BeginInvokeFailed for how fire-and-forget failures are reported |
Events
| Name | Args | Description |
|---|---|---|
BeginInvokeFailed |
BeginInvokeFailedEventArgs |
Raised on the owner thread, after DoInvokes has run its pending work, once per TryBeginInvoke action that threw. With no handler attached, DoInvokes throws an AggregateException of those failures instead. If a handler throws, it still receives every later failure, every other handler still receives the failure it threw on, and DoInvokes then throws an AggregateException holding each handler exception followed by the failure it was handling. An action TryBeginInvoke ran inline on the owner thread is reported the same way: with the pump's other failures when it ran inside DoInvokes (for example from an InvokeAsync delegate), or at once from TryBeginInvoke otherwise, which then throws the AggregateException itself when no handler is attached |
Advanced Usage
Thread Synchronization
Invoker provides a clean way to synchronize access to resources that must be accessed from a specific thread:
public class ResourceManager
{
private readonly Invoker _invoker = new Invoker();
private readonly Dictionary<string, Resource> _resources = new Dictionary<string, Resource>();
// This method can be called from any thread
public Resource GetResource(string id)
{
return _invoker.Invoke(() => {
if (!_resources.TryGetValue(id, out var resource))
{
resource = new Resource(id);
_resources[id] = resource;
}
return resource;
});
}
// Call this regularly on the owner thread
public void Update()
{
_invoker.DoInvokes(); // Process any pending resource requests
// Update resources
foreach (var resource in _resources.Values)
{
resource.Update();
}
}
}
Choosing the Owner Thread and Queue Capacity
The thread that constructs an Invoker owns it, and that can't be changed later. To own it from a
different thread, such as a dedicated render thread, construct it on that thread. The optional
constructor argument sets the capacity of the non-blocking TryBeginInvoke queue (default 1024,
rounded up to the next power of two):
Invoker? renderInvoker = null;
Thread renderThread = new(() =>
{
// This thread now owns the invoker, and is the one that must call DoInvokes()
renderInvoker = new Invoker(beginInvokeCapacity: 4096);
// ... render loop that calls renderInvoker.DoInvokes() each frame
});
renderThread.Start();
Contributing
Contributions are welcome! Here's how you can help:
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please make sure to update tests as appropriate and adhere to the existing coding style.
License
This project is licensed under the MIT License - see the LICENSE.md file for details.
Acknowledgements
Special thanks to all contributors and the .NET community for their support.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Memory (>= 4.6.3)
- System.Threading.Tasks.Extensions (>= 4.6.3)
-
.NETStandard 2.1
- No dependencies.
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages (3)
Showing the top 3 NuGet packages that depend on ktsu.Invoker:
| Package | Downloads |
|---|---|
|
ktsu.ImGui.App
A comprehensive .NET library that provides complete application scaffolding for Dear ImGui applications, featuring window management, DPI-aware rendering, precision PID-controlled frame limiting with comprehensive auto-tuning, advanced font handling with Unicode/emoji support, texture management, and debug tooling. Built on Silk.NET for cross-platform OpenGL support and Hexa.NET.ImGui for modern Dear ImGui bindings. |
|
|
ktsu.ImGuiApp
A comprehensive .NET library that provides complete application scaffolding for Dear ImGui applications, featuring window management, DPI-aware rendering, precision PID-controlled frame limiting with comprehensive auto-tuning, advanced font handling with Unicode/emoji support, texture management, and debug tooling. Built on Silk.NET for cross-platform OpenGL support and Hexa.NET.ImGui for modern Dear ImGui bindings. |
|
|
ktsu.ImGui.App.Testing
Headless test harness for ktsu.ImGui.App applications. Renders through a CPU rasterizer with no window, no GPU and no graphics driver, injects input directly into ImGui rather than through the operating system, and advances frames under the test's control so results do not depend on timing. Widgets are addressed by name rather than by coordinate, and rendered frames are captured for measurement in assertions and as artifacts on failure. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.5.2-pre.1 | 0 | 11/11/2026 |
| 1.5.1 | 38 | 11/11/2026 |
| 1.5.0 | 48 | 11/11/2026 |
| 1.4.4 | 4,810 | 10/9/2026 |
| 1.4.3 | 161 | 10/9/2026 |
| 1.4.3-pre.2 | 79 | 10/8/2026 |
| 1.4.3-pre.1 | 76 | 10/6/2026 |
| 1.4.2 | 1,516 | 10/6/2026 |
| 1.4.2-pre.1 | 80 | 10/5/2026 |
| 1.4.1 | 1,387 | 9/29/2026 |
| 1.4.0 | 189 | 9/29/2026 |
| 1.3.1-pre.1 | 72 | 9/28/2026 |
| 1.3.0 | 741 | 9/26/2026 |
| 1.2.38 | 843 | 9/24/2026 |
| 1.2.37 | 380 | 9/23/2026 |
| 1.2.36 | 488 | 9/22/2026 |
| 1.2.35 | 380 | 9/22/2026 |
| 1.2.34 | 128 | 9/21/2026 |
| 1.2.33 | 313 | 9/18/2026 |
| 1.2.32 | 628 | 9/17/2026 |
## v1.5.1 (patch)
Changes since v1.5.0:
- Report an owner-thread TryBeginInvoke failure instead of throwing it to the caller [patch] (#99) (matt-edmondson)