Asynchronous Messaging
Is it possible to convert a Qt/C++ code that uses threads executing an event loop and using the signal/slot mechanism with queued connections to communicate between 2 threads ?
The scope of your question is quite broad, so I'll try to point you in the right direction. Let's start with the signal/slot mechanism, from the Qt documentation:
Signals and slots are used for communication between objects. The signals and slots mechanism is a central feature of Qt and probably the part that differs most from the features provided by other frameworks. Signals and slots are made possible by Qt's meta-object system.
Needles to say, you'll have to migrate to one of DotNet's messaging frameworks to implement similar functionality. Based on your current set up and requirements:
- Setting up the connections:
QObject::connect(m_comManager, &CommunicationManager::dataRcvd,
this, &Application::onDataRcvd);
QObject::connect(m_comManager, &CommunicationManager::error,
this, &Application::showError);
QObject::connect(m_comManager, &CommunicationManager::progress,
this, &Application::setProgress);
- Message based interactions:
QMetaObject::invokeMethod(m_comManager , "setContinue",
Qt::QueuedConnection, Q_ARG(bool, false));
... that avoids me to use locks and their drawbacks.
You might want to look into the Actors model, such as provided by Akka.NET.
SynchronizationContext
I saw that there is this class called Dispatcher in C#/WPF (or WinForms), and I don't know if it exists apart from the UI context (can it be used in a .NET Console App or a Core App ?).
It can, and we'll get into that in a bit, but I don't think it maps well to your use case. As you know, the backing SynchronizationContext
usually marks threads with special abilities, such as the main thread in a GUI application. Thanks to this context, the async
/ await
pattern can be used to easily offload work and apply the results back on the main thread. Especially the bi-directional nature of the desired communication between your two threads, is more high-level than what's provided by a SynchronizationContext
. A better understanding of the inner workings might help. First, some context...
Console Synchronization
The Console
class synchronizes the use of the output stream, so you can write to it from multiple threads. In case of a Console Application, this means no two threads can write to the screen at the same time. As there's no need, you'll find SynchronizationContext
is not set on the main thread. The main thread is the only foreground thread, with workers being retrieved from the ThreadPool
(using default settings and with custom thread creation aside).
In GUI applications the main thread is the one with the SynchronizationContext
. Each time a Task
is created / async
is await
-ed on the main thread, this SynchronizationContext
is stored on the Task
so it can be retrieved when the Task
completes. If the captured SynchronizationContext
was null, then the continuation will be scheduled by the original TaskScheduler
(which is often TaskScheduler.Default
, meaning the ThreadPool
).
Synchronous execution
Let's start with an important observation. Using await
doesn't necessarily mean we're dealing with concurrency. In the following sample we query the id of the current thread using Thread.CurrentThread.ManagedThreadId
.
namespace ConsoleApp
{
internal class Program
{
public static async Task Main()
{
Console.WriteLine(GetCurrentThreadId());
await DemoAsync();
Console.ReadKey();
}
private static Task DemoAsync()
{
Console.WriteLine(GetCurrentThreadId());
return Task.CompletedTask;
}
private static int GetCurrentThreadId() => Thread.CurrentThread.ManagedThreadId;
}
}
Output:
1
1
Which is the id of the main thread. So as long as we don't await
any async
methods in the awaited DemoAsync
all code runs synchronously. Put otherwise, we never leave the main thread we started on. The code emphasizes this through the absence of the async
keyword in DemoAsync
's signature.
Asynchronous execution
Let's modify the previous sample to illustrate the difference.
namespace ConsoleApp
{
internal class Program
{
public static async Task Main()
{
Console.WriteLine(GetCurrentThreadId());
await DemoAsync();
Console.ReadKey();
}
private static async Task DemoAsync()
{
Console.WriteLine(GetCurrentThreadId());
await Task.Yield();
Console.WriteLine(GetCurrentThreadId());
}
private static int GetCurrentThreadId() => Thread.CurrentThread.ManagedThreadId;
}
}
Output:
1
1
3
The last id may vary in your output. Task.Yield()
creates a Task
that, when awaited, will return control to the context from which it was created. However, the use of Task.Yield()
is somewhat special in case of a Console Application. Given SynchronizationContext
is not by default set, the continuation doesn't know where to yield control back to. So instead it remains on the current context, which is that of the (used thread from the) ThreadPool
.
Of course, we could have awaited any async
method to get similar results, but it provides a nice introduction to the following segment.
SynchronizationContext
Ref Await, SynchronizationContext, and Console Apps | .NET Parallel Programming (microsoft.com)
Following sample from the referenced post demonstrates how control is never given back to the main thread. Only the code leading up to the first Task.Yield()
runs on the main thread. From then on, only threads from the ThreadPool
are used.
namespace ConsoleApp
{
internal class Program
{
public static async Task Main()
{
await DemoAsync();
Console.ReadKey();
}
private static async Task DemoAsync()
{
var d = new Dictionary<int, int>();
for (var i = 0; i < 10000; i++)
{
var id = GetCurrentThreadId();
d[id] = d.TryGetValue(id, out var count) ? count + 1 : 1;
await Task.Yield();
}
foreach (var pair in d) Console.WriteLine(pair);
}
private static int GetCurrentThreadId() => Thread.CurrentThread.ManagedThreadId;
}
}
Output (yours will vary):
[1, 1]
[3, 3087]
[4, 3292]
[5, 2667]
[6, 953]
To let yield find home, we're going to implement a custom SynchronizationContext
. This is not something you'll find yourself doing often, as each platform typically provides their own custom implementation. We use BlockingCollection
because it's a good candidate for our message pump. Not only does it have queue
semantics by default, it also blocks the caller when the queue is empty.
namespace ConsoleApp
{
public sealed class SingleThreadSynchronizationContext : SynchronizationContext
{
private readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> _queue =
new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();
public override void Post(SendOrPostCallback d, object state) =>
_queue.Add(new KeyValuePair<SendOrPostCallback, object>(d, state));
public void RunOnCurrentThread()
{
foreach (var workItem in _queue.GetConsumingEnumerable())
{
workItem.Key(workItem.Value);
}
}
public void Complete() => _queue.CompleteAdding();
}
}
Next, in our updated sample, we first store the current context (which, again, in a Console Application will be null
) and then create our custom SynchronizationContext
and set that as the context of the current thread. The execution of DemoAsync
will now remain on the main thread during its iteration.
namespace ConsoleApp
{
internal class Program
{
public static void Main()
{
var prevCtx = SynchronizationContext.Current;
try
{
var syncCtx = new SingleThreadSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncCtx);
var t = DemoAsync();
t.ContinueWith(_ => syncCtx.Complete(), TaskScheduler.Default);
syncCtx.RunOnCurrentThread();
t.GetAwaiter().GetResult();
}
finally
{
SynchronizationContext.SetSynchronizationContext(prevCtx);
}
Console.ReadKey();
}
private static async Task DemoAsync()
{
var d = new Dictionary<int, int>();
for (var i = 0; i < 10000; i++)
{
var id = GetCurrentThreadId();
d[id] = d.TryGetValue(id, out var count) ? count + 1 : 1;
await Task.Yield();
}
foreach (var pair in d) Console.WriteLine(pair);
}
private static int GetCurrentThreadId() => Thread.CurrentThread.ManagedThreadId;
}
}
Output:
[1, 10000]
Wrapping up
In DotNet the creation of custom threads is often discouraged, in favor of using threads from the ThreadPool
. There are dedicated threads for both CPU-intensive and I/O-bound tasks, so heavy load on one won't influence the other. In short, you don't really (need to) care which thread handles your request. That doesn't mean that the Actor model with, amongst others, it's lock free approach, doesn't have its benefits. It's just not the native one.
Other resources
DispatcherSynchronizationContext (WindowsBase.dll: System.Windows.Threading) WPF and Silverlight applications use a DispatcherSynchronizationContext, which queues delegates to the UI thread’s Dispatcher with “Normal” priority. This SynchronizationContext is installed as the current context when a thread begins its Dispatcher loop by calling Dispatcher.Run. The context for DispatcherSynchronizationContext is a single UI thread.
All delegates queued to the DispatcherSynchronizationContext are executed one at a time by a specific UI thread in the order they were queued. The current implementation creates one DispatcherSynchronizationContext for each top-level window, even if they all share the same underlying Dispatcher.
Default (ThreadPool) SynchronizationContext (mscorlib.dll: System.Threading) The default SynchronizationContext is a default-constructed SynchronizationContext object. By convention, if a thread’s current SynchronizationContext is null, then it implicitly has a default SynchronizationContext.
The default SynchronizationContext queues its asynchronous delegates to the ThreadPool but executes its synchronous delegates directly on the calling thread. Therefore, its context covers all ThreadPool threads as well as any thread that calls Send. The context “borrows” threads that call Send, bringing them into its context until the delegate completes. In this sense, the default context may include any thread in the process.
The default SynchronizationContext is applied to ThreadPool threads unless the code is hosted by ASP.NET. The default SynchronizationContext is also implicitly applied to explicit child threads (instances of the Thread class) unless the child thread sets its own SynchronizationContext. Thus, UI applications usually have two synchronization contexts: the UI SynchronizationContext covering the UI thread, and the default SynchronizationContext covering the ThreadPool threads.