-
Notifications
You must be signed in to change notification settings - Fork 62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Question about executing context #35
Comments
Hi @durkmurder, It looks similar to #20 but with a different use case. I also find myself doing this check a lot and I agree that we may need to make it easier to handle. I already thought about your option 3, however, even if it seems the most natural way to implement it, I don't like it for the reason explained in this comment. Option 3 (the one you suggested): 3.1: the context object is needed in all continuation methods (i.e. 3.2: promise.via(context).then([context](res) { ... }) Option 4 (one solution I implemented in my projects): It's quite similar to your option 1 but using a new // `this` is a QObject based class with a process() method:
auto ctx = QtPromise::context(this); // or QtPromise::context(std::shared_ptr<T>*), or...
promise.then([ctx](result) {
ctx->process(result);
})
// ... and optionally ...
.fail([](const QPromiseContextException& e) {
// handle the destroyed context if needed
}) Option 5 (just thought about it while writing this reply): Maybe we could wrap the callback using a promise.then(QtPromise::bind(this, [](ctx, result) {
ctx->process(result); // ctx being `this`
}).fail([](const QPromiseContextException& e) {
// handle the destroyed context if needed
}) To sum up:
Thoughts ? |
I was inspired by Folly futures, they are very flexible in terms of executing context of continuations. I think breaking current API is not desired. For me personally I would like this flow: I am also ok with options 4 and 5(they are kinda the same as I see), but they add a lot of boilerplate even if you do simple |
Folly
The empty
While usage of options 3.2 and 4 are very similar (see below), with option 3.2, the promise returned by // 3.2 (.via() renamed to .bind())
QPromise<int> Foo::execute() {
return promise
.bind(this)
.then([this](int res) { this->process(res); })
.fail([this](int err) { this->handler(err); })
.finally([this]() { this->finalize(); });
} vs // 4
QPromise<int> Foo::execute() {
auto ctx = QtPromise::context(this);
return promise
.then([ctx](int res) { ctx->process(res); })
.fail([ctx](int err) { ctx->handler(err); })
.finally([ctx]() { ctx->finalize(); })
//...
} |
I should have made it clear earlier, talking about context I meant 4 looks good just the error handling is a little bit too much. You basically get the same problem with general |
Your first example doesn't reflect the where because the Option 4 is already implemented in a local branch (just missing the docs). I haven't contributed it yet because I'm still unsure that's the best API, so I'm really interested in feedback / alternatives that work well with all use cases. |
Of course. Why I am talking about signals & slot it's because of next behavior Adding context support with if and where support the same way signals & slots are resolved will give great flexibility.
Something like this can be written:
I am not really sure that's the exact syntax, but I do those manipulations with |
Ok, I fully get it now, thanks! So indeed, option 4 doesn't address the where part. Option 5 would work but I agree that it requires more code (though I still like it because it doesn't impact internals). I would probably pick another name than foo->execute()
.then([] { ... }) // should be call in the current thread, whatever the thread of m_context in execute().
.fail([] { ... }); // should be call even if m_context used in execute() is destroyed. Maybe supporting QPromise<int> Foo::execute() {
return m_context->asyncCall()
.bind(m_context)
.then([m_context](int res) { m_context->process(res); })
.fail([m_context](int err) { m_context->handler(err); })
.finally([m_context]() { m_context->finalize(); })
.unbind();
} |
Still thinking about option 5: it's very close to 3.1 and to me, it's less error prone than 3.2 because it doesn't need to QPromise<int> Foo::execute() {
return m_context->asyncCall()
.then(QtPromise::bind(m_context, [m_context](int res) { m_context->process(res); }))
.fail(QtPromise::bind(m_context, [m_context](int err) { m_context->handler(err); }))
.finally(QtPromise::bind(m_context, [m_context] { m_context->finalize(); }));
} instead of: QPromise<int> Foo::execute() {
return m_context->asyncCall()
.then(m_context, [m_context](int res) { m_context->process(res); })
.fail(m_context, [m_context](int err) { m_context->handler(err); })
.finally(m_context, [m_context] { m_context->finalize(); });
}
|
I agree that |
Hi Simon, this is an interesting problem indeed, I had stumbled upon it once but solved in a way that ensured that my QObjects outlive the promises. Ad "if"-Issue: I like the idea of the automatic fail if the "context" object is destroyed. It would be consistent with the Qt framework behavior. We could probably add one or more "watched" QObjects at the beginning of the promise chain, so that there is no need in either "bind" or "via" calls or extra parameters. Something like: class Foo : public QObject
{
QPromise<int> bar()
{
return QPromise<int>{this,
[](auto resolve, auto reject) {
// ...
}
};
}
};
Foo foo;
foo.bar()
.then([](int r) {
return QString::number(r);
})
.then([](QString s) {
// ...
})
.fail([](const QPromiseContextException&) {
})
.fail([]() {
}); I like that the handling of the Ad "where"-Issue: I think that the
I think the 1. is the most important even if it leads to the duplication of the chain functions. At the end of the day, the library should be easy to use rather than easy to develop. As for the general idea of "where" and multithreading, it seems to me that the computation is being transferred into a chained promise handler using the thread affinity of the context object instead of being encapsulated in the object itself. I would probably refactor the application in a way that the computation is performed in some closed algorithms using the QtConcurrent in the promise chain. Or, if these are some long-living objects, this could be a case of the agent model of computation, so one should remain at message passing (signals/slots). In this case, we should either do the class Foo : public QObject {};
QtPromise::resolve(42).then(QtPromise::bind(&foo, &Foo::method1, _1)); In this case, the Cheers |
Hi Dmitriy, Thanks for your feedback. I believe that context has to handle both if and where if used as Promise<void> ChainManager::loadChains(std::vector<AssetID> chains)
{
return Promise<void>([=](const auto &resolver, const auto &reject) {
QMetaObject::invokeMethod(_executionContext, [=] {
try
{
// do something
resolver();
}
catch(std::exception &ex)
{
reject(ex);
}
});
});
} Note: Somewhere from UI thread {
chainManager.loadChains({}).then([this] {
// do something on main thread.
}) ;
} Generally it works ok since I make sure that context is valid or add some extra checks. QtPromise::resolve(this).then(_executionContext, [] {
// All logic here
// Without need of new promise and explicit exception handling
} Hopefully it makes sense. |
Hey Yura, that's an interesting use case indeed. So if I understand correctly, you queue work units into an existing thread worker and perform postprocessing after a particular work unit is ready, thus organizing a pipeline. That's quite an ingenious solution with For what it's worth if you don't need to share the worker's state between the work units, you could remove the thread awareness of the worker and instantiate it in a QtConcurrent call. I solved a file copying problem with a progress report in this manner: // a pseudocode snippet, by a happy coincidence looking like C++/QtPromise
QPromise<void> Foo::copyFiles(QVector<QString> fileList, QString destination)
{
return QtPromise::resolve(QtConcurrent::run([this, fileList, destination]() {
// now in a thread from the global thread pool
// FileWorker is unaware of threads, it just copies files sequentially and emits signals
FileWorker worker;
connect(&worker, &FileWorker::progressChanged, this, &Foo::copyProgressChanged);
worker.copy(fileList, destination); // may throw, or return a bool and throw here
});
} Transferring it to your example, the processing logic involved in the But, again, if you have to share the state of the Cheers |
Hi Dmitriy |
Hey @simonbrunel, any input on last comments? |
Hi guys! Thank you for your feedback, it's really interesting to learn about how you use QtPromise. And sorry for the late reply, I'm currently focused on something else far from this lib. Note that I'm on QtMob slack if you want a more interactive exchange :)
That's a really good point which makes me reconsider option 3.1. Though, I'm still concern about maintainability if this new pattern
That's personal but I would consider About having context part of the promise constructor: to me, dealing with a context is because we need to access it from a continuation (async) callback. The constructor callback is executed synchronously and I don't see any reason to invalidate the entire promise chain later if the context isn't needed by the rest of the chain. Though, I'm open for discussion if you think there is legit use cases for it but maybe on Slack or another GH ticket. It looks like option 3.1 is the favorite and would solve most use cases. It's similar to the |
I use
qtpromise
a lot in my project and I am constantly running into next situationSo it can happen that Foo gets deleted before promise is resolved, and since function is called as callback I need to check if
this
is still valid in some way otherwise I get segfault.I was thinking a lot about it and it seems like a pretty common thing.
I see 3 ways how to resolve this situation.
this
as I am doing it right now.I like 3rd case the most but it requires changes to the library(which I can do). I want to discuss here if it makes sense at all and if you see any problems with it.
So the idea is to provide executing context to continuations, introduce new call like,
thenVia(QObject *, callback)
or something likethen(callback).via(QObject*)
which will be used as context for delivering callback. If such functionality exists then my use case will be resolved just by using context for continuations since if context is already deleted, callback won't be executed(same behavior with slots). Implementation wise it shouldn't be that hard to do it, since it requires just some modifications toqtpromise_defer
.Looking forward to discussing it
The text was updated successfully, but these errors were encountered: