Non-blocking QML



  • Situation
    You either have a QObject derivative that exposes a Q_INVOKABLE method to QML, say myapi.myfunc.. or you have a pure JavaScript function myjsfunc defined in the QML code. In both you do some heavy calculations and/or (network) I/O.

    Problem
    First: both myapi.myfunc and myjsfunc are asynchronous and needs proper async handling
    Second: You don't want neither myapi.myfunc nor myjsfunc to block the event loop while it's executing

    Possible Solution
    Return a Promise object! and let the user define the handling on it. In QML/JavaScript you can then define some asynchronous logic this way:

    import "myfsfunc.js" as JsBusinessLogic
    
    SomeItem {
    	onSomething: {
    		myapi.myfunc(...)
    		.then(
    			function(firstResult) {
    				//success! make another async call
    				return JsBusinessLogic.myjsfunc(firstResult)
    			},
    			function(err, msg) {
    				//failure! myfunc returned an error
    			}
    		)
    		.then(
    			function(secondResult) {
    				//success! second call succeeded, EXIT
    			},
    			function(err, msg) {
    				//failure! second call returned an error  
    			}
    		)
    		.catch(function() {
    			//something went seriously wrong
    		})
    	}
    }
    

    Meanwhile myjsfunc could look somewhat like this:

    function myjsfunc() {
    	return new Promise(function(resolve, reject) {  
    		//do some heavy lifting...
    		if(result == good) {
    			resolve(result)
    			return
    		}
    		reject("baaaad results, error!")
    	})
    }
    

    The promise object allows easy chaining of async operations and proper error and exception handling, but it still wont't change the fact that it's executed by the event loop and will inevitably block it, which ofcourse is unacceptable.

    Question
    So.... to make the async Promise non-blocking, I'd need to implement the Promise class in a way that uses a thread pool to execute the Promises in the background?
    This way when you call an invokable C++ method or a JS function both create and return a Promise object and exit! not blocking the loop any longer!, right?!
    The promise itself is then executed in a separate thread from the thread pool in the background. When it's finished its calculation and/or waiting for external resources it will run the JS handlers defined by then() or catch() in the event loop again, which won't block much.

    So did I correctly understand it? Would such kind of a Promise implementation work as described? Or is there probably an easier way?

    Thanks!


    P.S.
    You might ask me why I'm implementing parts of the business logic in pure JavaScript when JS is not meant to be used for this in Qt? Well... you see.. executing 3rd party C++ is not an option if you want to build a safe OS basis, that's why JavaScript does the logic, JavaScript is easily sandboxed by the QQmlEngine while C++ is extraordinary hard to sandbox. Also using JavaScript for logic like this is what it was eventually designed for.. it's a scripting language and I use it to glue low-level modules and APIs together, just in an asynchronous, non-blocking way.



  • This is the kind of things you become Qt Champion for: https://github.com/benlau/quickpromise

    P.S.
    Big up to the way you ask questions!



  • @VRonin QuickPromise seems to use a timer to push the execution onto the callback queue, but the actual promise body will still run in the stack on the QML event loop. But in my case the promise created by an API method must be executed on a separate thread as it would otherwise block the even loop when doing I/O and heavy lifting and only then asynchronously push the then() callbacks onto the event queue.

    libuv, the lib that powers NodeJS uses a thread pool backed event loop as far as I know. So I imagine that I'd need another event loop behind the actual QML event loop to execute Promises on different threads. Every time a new promise is created either in C++ or in QML it is pushed to the event queue of the backend event loop, where it is pulled onto the stack of one of the worker threads out the pool and executed, when it's finished it then pushes the registered callback functions onto the QML's event queue, which in turn might also create another promises which are also processed by the backend event loop and so on...