-
-
Notifications
You must be signed in to change notification settings - Fork 60
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
Multitasking #166
Multitasking #166
Conversation
The module must be named All other I would also like to have |
Thanks, this made me consider an alternative way to possibly do it. It's added to the PR. This avoids the module naming question altogether, so if we like this variant we can save the module naming philosophy for a call 😉 Since
This allows the exact same use cases but fits in better with the rest of our API. It's also exactly how it is implemented. Borrowing from the example we'd get: from pybricks.tools import Task, run_task
# This is the main task
async def main():
print("Running multiple tasks at once!")
await Task(square(), blink(), hello("Pybricks"))
print("You can also just run one task.")
await hello("World!")
# Run the main task.
run_task(main()) You could even take this a step further by letting from pybricks.tools import Task # One less import this way
# If it was a method.
Task(main()).run()
# If it was a class method.
Task.run(main()) But I figured that a separate function called
|
Fewer modules and functions is always a good thing. Why does the replacement for all/race need to be a class? Alternate naming ideas: from pybricks.tools import multitask, run_async
...
async def main():
await multitask(do_thing(), do_other_thing())
run_async(main()) Implementation detail ideas:
|
It doesn’t strictly have to be a class, I’m just thinking it probably does not need to be a module that is imported from another module, as done in earlier drafts. It seems that we mainly did that to nicely group run/all/race with some sort of common namespace. But with something like For what it’s worth, it is a class in that it creates a new task that runs a set of given tasks in parallel. It is also implemented as a class. It instantiates an object that you can await. But we could indeed make a wrapper function that gives you an instance of that class. In the C implementation this could be done by just giving the type a lower case name so we don’t have to add an explicit wrapper function. I like the lowercase names you suggested though, so I might pull those in. Indeed, helpful errors would make this easier too. |
On sensors, we were hoping to avoid This is because they do not only have mode switches, but also writes for light controls. So except for maybe the force sensor, pretty much everything would need |
a71130c
to
436738d
Compare
Isn't that a separate method from reading though? Also, lights could be reworked to use a background process for I/O similar to how the built-in hub lights with SPI or I2C busses already work. |
Now that we have finalizers enabled, I think it would be fairly easy to drop the "external" light implementation and use the "internal" light implementation with dynamically allocated light structures. |
Reading after setting the lights means switching back to one of the measuring modes. |
I guess async/blocking get value methods would work better for I2C sensors on EV3/NXT too since we could perform the I2C transaction right then instead of having a background polling process. |
Indeed. I've come to think that it is justifiable to use await for anything that uses communication with a sensor. I think we may just have to make the documentation very explicit about it for each method that needs it. Currently working on some ideas to generalize the common awaitable type to implement this relatively cheaply. |
ba9e9f4
to
c881dc2
Compare
I've done some additional cleanups and refactoring. This should make it easy to add async support for sensors and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking really good.
I think the most difficult problem remaining is solving cancellation/closing that takes some time. In Python, this is solved using async with
, but I don't the we want to have to write that all of the time. In asyncio.streams
, there is a wait_closed() method for this sort of thing, but again an extra call that would be nice to avoid.
* Flag indicating controller should run until it stalls. Can be used to | ||
* explicitly run until it stalls to find a mechanism endpoint. | ||
*/ | ||
PBIO_CONTROL_TYPE_FLAG_OBJECTIVE_IS_STALL = 1 << 3, | ||
} pbio_control_type_t; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be made a bit safer to use by leaving the existing enum as-is and adding instead packing the data using a new:
typedef struct {
pbio_control_type_t type : 2;
bool stop_on_stall : 1;
bool objective_is_stall : 1;
} pbio_control_type_and_flags_t
pybricks/common/pb_type_control.c
Outdated
@@ -159,16 +159,16 @@ STATIC mp_obj_t pb_type_Control_pid(size_t n_args, const mp_obj_t *pos_args, mp_ | |||
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Control_pid_obj, 1, pb_type_Control_pid); | |||
|
|||
// pybricks._common.Control.target_tolerances |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need to change comment to match rename
pybricks/common/pb_type_control.c
Outdated
@@ -292,8 +261,7 @@ STATIC const pb_attr_dict_entry_t pb_type_Control_attr_dict[] = { | |||
STATIC const mp_rom_map_elem_t pb_type_Control_locals_dict_table[] = { | |||
{ MP_ROM_QSTR(MP_QSTR_limits), MP_ROM_PTR(&pb_type_Control_limits_obj) }, | |||
{ MP_ROM_QSTR(MP_QSTR_pid), MP_ROM_PTR(&pb_type_Control_pid_obj) }, | |||
{ MP_ROM_QSTR(MP_QSTR_target_tolerances), MP_ROM_PTR(&pb_type_Control_target_tolerances_obj) }, | |||
{ MP_ROM_QSTR(MP_QSTR_stall_tolerances), MP_ROM_PTR(&pb_type_Control_stall_tolerances_obj) }, | |||
{ MP_ROM_QSTR(MP_QSTR_tolerances), MP_ROM_PTR(&pb_type_Control_tolerances_obj) }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this a breaking change?
pybricks/common/pb_type_motor.c
Outdated
} | ||
|
||
if (ex != MP_OBJ_NULL) { | ||
nlr_raise(ex); | ||
// Get torque limit as percentage of max torque. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use SI units for torque?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then you'd need to look up the torque for each motor. I'm torn about this one --- a percentage is very convenient for this use case as it is only a temporary setting (and it is a percentage of something well defined).
// Start moving. | ||
pb_assert(pbio_servo_run_until_stalled(self->srv, speed, torque_limit_pct, then)); | ||
|
||
// Handle completion by awaiting or blocking. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Handle completion by awaiting or blocking. | |
// Handle completion returning awaitable or blocking depending on run loop state. |
|
||
return mp_const_none; | ||
self->notes_generator = mp_getiter(notes_in, NULL); | ||
self->note_duration = 4 * 60 * 1000 / pb_obj_get_int(tempo_in); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deleted comment is helpful to explain why we are using these specific numbers.
|
||
// Require that duration is nonnegative small int. This makes it cheaper to | ||
// test completion state in iteration loop. | ||
time = pbio_int_math_bind(time, 0, INT32_MAX >> 2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
time = pbio_int_math_bind(time, 0, INT32_MAX >> 2); | |
time = pbio_int_math_bind(time, 0, MP_SMALL_INT_MAX); |
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_tools) }, | ||
{ MP_ROM_QSTR(MP_QSTR_wait), MP_ROM_PTR(&tools_wait_obj) }, | ||
{ MP_ROM_QSTR(MP_QSTR_StopWatch), MP_ROM_PTR(&pb_type_StopWatch) }, | ||
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_tools) }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is less noise in the long run if we don't try to align the braces.
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_tools) }, | |
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_tools) }, |
return; | ||
} | ||
|
||
mp_obj_list_t *awaitables = MP_OBJ_TO_PTR(awaitables_in); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this safe without a type check?
|
||
// Outside run loop, block until the operation is complete. | ||
while (test_completion_func && !test_completion_func(obj, start_time)) { | ||
mp_hal_delay_ms(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 On ev3dev (at least with the current implementation) we need a delay here. But in general, MICROPY_EVENT_POLL_HOOK
would be more appropriate here.
Good to know. That is encouraging for taking this further 😄
Indeed. The direction I am currently heading in is ensuring cancellation can be instant. This seems to be possible so far for motors and I think sensors after the revision I'm currently working on. In areas where it is harder (maybe like Bluetooth), we could consider being more strict about not allowing multiple uses on parallel "tasks" and raising an exception instead. |
a81e39a
to
ffb651a
Compare
This function can be called from the pure-Python task module to inform blocking methods that they must return an awaitable generator.
This adds a new MotorWait object that is generator-like (send() and throw() are omitted since we shouldn't need them) to provide a non- blocking way to wait for completion of a motor operation.
This reworks the blocking case of run_until_stalled to just use the generator approach as well. This reduces code duplication. This would get even simpler if we didn't have handle the exception here but restored the voltage on cancellation in general.
This prepares for refactoring to reduce allocation of generator objects.
This only allocates a new generator if all previous generators are in use. In a "good" program with no conflicting motor tasks, this means nothing is allocated outside the motor object at all.
This prepares for adding additional control objectives as flags.
This implements running until stalled.
This will be used to replace the module-level wrapper around running forever and awaiting a stall condition. This simplifies those routines because it can just wait for completion just like any other run_* method. It is also useful to have this functionality (back) in pbio so that non-MicroPython implementation can still use this.
Stopping on stall and running until stall are different things.
This uses the new pbio implementation. It also renames the duty_limit kwarg but allows the original name for backwards compatibility.
In many cases, the await part is the same across methods and only the return value differs. This generalizes the awaitable by splitting the completion test and return value. This will be particularly useful for sensors later on. All methods await mode switches in the same way, but they have different return values.
With the return value available separately, we can get it directly instead of through iterating. This reinstates behavior close to the way it was before introducing async, in an effort not to reduce performance in the normal use case.
No longer needed now that run_until_stalled is handled in pbio.
These configs were only used to group function parameters. Since they will not be constant generally (each method may have its own return value getter), it is better to just keep the lengthy function args. As done in the drivebase and motor, they can still have a common function that populates the awaitable to avoid code repetition, which keeps code size about the same.
This broke while refactoring the awaitable code previously.
…n linked list. This avoids issues with garbage collected items of the linked list. It also makes the code easier to follow by using standard MicroPython tools.
This allows us to await processes without going through an explicit MicroPython object. This doesn't make a difference for the awaitables implemented so far, but this will be useful for Bluetooth and sensor awaitables. For example, for pupdevices the void *object can be the *iodev so that the same awaitable type can be used for all sensors, even though each of the sensor_obj_t are all slightly different.
This generalizes it to awaitable options instead of options strictly about cancellation. No code functionality is changed in this commit.
Instead of start time. This allows for implementing a timeout without increasing the allocated size of the awaitable. It also means we can correctly store the time for a wait instead of hacking the duration into a pointer type.
Prepares for handling via async.
We want to call this from most class inits.
This is an alternate way to use temporary limits in pbio (no nlr buf) but without a breaking change in the definition of duty limit or stall. See pybricks/support#1069
This lets us assign a keyboard shortcut in user settings to build it quickly.
This reverts commit 2e19eef90cf7590676d65f0e37a91b7bbb867d52.
Also rename so it doesn't mask the data of the process thread.
This does not need the object itself. Also fix enum names. It will also be used for operations other than cancel, so rename it.
Until we work out clever ways of cancellation or queueing, we can prevent certain resources from being used on more than one task at all.
The initialization code ran only on boot, which could cause it to get in a bad state the second time.
This has been merged to get early testing in and make some quick progress. Now we'll be iteratively testing and improving it, and optimizing for code size. |
This is an early exploration to implement pybricks/support#917. See that issue for rationale and motivation. We can use this pull request to dig more into the technical details.
I normally like to review it commit-by-commit, but in this case the commits are more for historical context. The later commits pretty much rewrite the earlier ones based on lessons learned over time.
Work in progress:
task
withrun
,all
, andrace
functionsrun
,all
, andrace
This pull request enables running scripts like the following. Scripts without
run_task
should run without user-facing changes.