1

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 ?

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 ?).

In Qt, I usually use this pattern : I have a class Application that holds the main window/widget and it launches a thread that handles non-UI stuff (acquisition from a DAQ card, communication with other devices etc...)

Application::Application(QObject *parent) :
    QObject(parent)
{
s_instance = this;
//....

// Register types so they can be exchanged via the signal/slot mechanism
    qRegisterMetaType<Data>("Data");

// Com. manager
    m_workerThread = new QThread(this);
    m_comManager = new CommunicationManager();


    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);

    m_comManager->moveToThread(m_workerThread );
    m_workerThread ->start();

//...
}

In the worker object, in some cases, I have a method that will execute a loop that can be interrupted if the UI requested that.

In the UI thread, I can request the worker thread to execute this function (like sending a mail) to stop the for loop :

QMetaObject::invokeMethod(m_comManager , "setContinue",
                                  Qt::QueuedConnection, Q_ARG(bool, false));

In the object "living" in a secondary thread, I have this public slot that is already executing (like setContinue, the request is sent through a mail/signal using invokeMethod) :

void CommunicationManager::processRequest()
{

// the for loop that can be interrupted from UI thread safely
for (size_t i = 0; i < numberOfLoops && !error && !isCanceled(); ++i)
        {//.... }
}
//...
}

bool CommunicationManager::isCanceled()
{
    auto const dispatcher = QThread::currentThread()->eventDispatcher();
    if (!dispatcher)
    {
        return false;
    }
    dispatcher->processEvents(QEventLoop::AllEvents);
    return !m_bContinue;
}

// The method that is requested (from UI) to be executed on the worker thread :
void CommunicationManager::setContinue(const bool continue)
{
    if (m_bContinue!= continue)
    {
        m_bContinue = continue;
    }
}

Is it possible to have this same of code (especially the part where the main thread sends a signal (e.g. a mail) to the secondary thread mail box (list of slots/events/callbacks to be processed) with C# (or even with the Java, I'm curious). If the answer is no, it's really sad, because that avoids me to use locks and their drawbacks.

Aminos
  • 754
  • 1
  • 20
  • 40

3 Answers3

1

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));
  • Mailbox queues:

... 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.

Funk
  • 10,976
  • 1
  • 17
  • 33
0

By embedding JVM in your C++ application you can call any Java method in your secondary thread using Java reflection.

See: this link

But in general, low-level implementations (in C, C++, etc..) are called from higher levels, not in verse direction.

raxetul
  • 439
  • 5
  • 12
  • I want to know if it's possible to do this natively (using a message loop for threads in C# and/or Java). Apparently the answer is no. In C#, there's async methods, cancellation tokens and maybe events too that are used but not that pattern that I can use with Qt. – Aminos May 11 '21 at 07:44
0

I posted a similar question here : Implementing a permanent thread that manages a resource and other threads that requests this thread to do some task and return a result and it has interesting resources and a final answer to my question.

Aminos
  • 754
  • 1
  • 20
  • 40