Conversation
|
Here some points:
|
You are rigth, try catch is needed.
If a task is canceled before entering the work queue, the "done" work would never be called because the execute callback would not be executed. It might make more sense to rename it to "finished" or "completed" instead of "done".
I'm not sure about this either. However I would like to add an on_exception callback, to call it when an exception happens in another callback.
Great, will do it! |
|
I've added a new function "try_skip" in d32eb3f, this function would be useful to remove work from the queue that wasn't started yet. |
|
To be sincere I'm not a big fan about making the Probably we can fix some bugs to make the impl even more simple, e.g. about:
We might change the current void task::in_worker_thread()
{
try {
//if (!m_token.canceled()) // removing this line to always call the execution callback
m_execute(m_token);
}
...About the The About all the other features/callbacks (finished, exception, done) I think are not necessary as they could be moved to another wrapper class/abstraction, e.g. the same |
I'm fine with this, I was just trying to keep the current behavior as much as possible by adding optional new behavior.
I'm happy to change it to any other name, so try_pop() is fine by me.
If I remove the friendship, then I got this error: |
|
Okay, trying the new changes now there is a caveat. This is our new void task::in_worker_thread()
{
m_state = state::RUNNING;
try {
m_execute(m_token);
}
catch (const std::exception& ex) {
LOG(FATAL, "Exception running task: %s\n", ex.what());
}
m_state = state::FINISHED;
}In my [this, func](base::task_token& token) {
try {
func(m_L);
}
catch (const std::exception& ex) {
// TODO: do something
}
m_finished(token);
}Now base::task::m_state is not FINISHED when my m_finished callback gets called. And I was actually removing the task when finished, but now it throws the |
Correct me if I'm wrong, but it looks like the finished state is a problem of your own task, but not a general state required by [](base::task_token& token)
{
try {
..."execute" code that checks the token and can be canceled...
}
catch (...) { ... }
..."finished" code that doesn't check the token and knows that task is finished...
}I don't like to add more complexity to this |
Only in debug mode it is being a problem. Because there is an ASSERT in the base::task's destructor that checks the task's state.
This is almost exactly what I've described in my last comment.
That's fine. The only thing I'm trying to do is to avoid that ASSERT I'm mentioning. This is how the scenario looks:
[this, func](base::task_token& token) {
try {
func(m_L);
}
catch (const std::exception& ex) {
// TODO: do something
}
m_finished(token);
}
task::~task()
{
// The task must not be running when we are destroying it.
ASSERT(m_state != state::RUNNING);
}So, I'm wondering if we should remove that assert or do something else. |
|
This is my issue summarized using your example code: [](base::task_token& token)
{
try {
..."execute" code that checks the token and can be canceled...
}
catch (...) { ... }
// Here the base::task doesn't know yet that we've finished, so if it is destroyed (as I'm doing),
// the assert in its destructor fails
..."finished" code that doesn't check the token and knows that task is finished...
} |
|
I'd prefer to avoid deleting the task inside a callback function itself (the token/task will become an invalid pointer). Generally in other If you think that we should allow this, removing the Another thing: why |
Exactly this is what I wanted to avoid with the new finished callback I was proposing. Because monitoring if the task has completed is actually more complex than having a callback to just let the client/caller know that the task finished.
Well yes, that is what I was trying to explain, I want to delete the task when finished, but since inside the execute callback it is not in FINISHED state yet, we need the callback. As I didn't want to do some monitoring stuff just for that, the introduction of the finished callback seemed to me a reasonable addition.
If the task is queued and you cancel the token it will be canceled only when the task is taken by some thread of the pool, but there was no way to cancel tasks waiting on the queue (btw try_skip is try_pop now as you suggested, just didn't push the changes yet because I was waiting for a decision about the finished callback). |
We'll be deleting the task inside a callback anyway.
"I want to delete the task inside the callback" would have saved a lot of time.
Not only the comment, the finished callback cannot receive the token as it doesn't make sense to cancel the task there. And the impl must make clear that deleting the task in the finished callback is allowed (so the task and the token are not going to be valid anymore and the code must be prepared for that).
I know, anyway the task will be almost immediately queued (the thread_pool is almost always free in our code). If you find it useful, keep the function. |
Yeah, I fully agree. In a callback that is executed after logical finalization of the task.
Certainly it is not useful for canceling the task there, nor modifying its data. But IMO the token is still useful in the finished callback because it holds information about how much progress was made by the task and how the task finished (by cancelation or success). What I do believe is that maybe we should pass a copy of the task_token to the finished callback to avoid any issue once the task is destroyed from within the callback.
In my use case I wanted to let the user stop scripts that are waiting in the thread pool's queue because the pool got exhausted by other scripts running infinite loops. |
d32eb3f to
6efb1ef
Compare
|
Pushed a rebased version of this PR, and also introduced the
|
|
I'll merge this as it is, I don't see the purpose of the copy of the token nor the finished callback, but I'll merge it anyway. |
If you don't see the purpose I would prefer to remove this changes then. I won't be mad, I thought you saw that it was useful at the end. I do see an advantage on being able to destroy a task when finished without the need of polling, but that is just my point of view. Also, I feel that the multiple scripts execution draft won't make it, because it needs an amount of changes that I don't think we are willing to make, in which case this changes won't be used as well. So, feel free to remove any change you don't think is useful. |
This PR introduces a couple of features to base::task:
This was introduced to be able to do some things in aseprite/aseprite#5037. Like being able to redraw the "Running Scripts" window as needed (when a task is started/done) and to avoid showing the "stop" button for enqueued tasks. Because we can't dequeue tasks (this should be something to add to base::task and base::thread_pool as well I think)
Something to think about: should we use an atomic enum for the base::task state instead of 3 different atomic bools? I believe that a task should be only in one of the following states at any given time: