@bgrommes: Your question exposed an important discrepancy between Objo’s intended concurrency model and what the VM was actually doing.
My earlier answers described the intended behaviour: synchronous Objo code should run uninterrupted until it returns control to the scheduler or reaches an Await that genuinely suspends. After reproducing the exact scenario you described, however, I discovered that the VM was processing queued task callbacks, timers, host events and desktop events between individual bytecode instructions.
It wasn't running two Objo threads simultaneously, but another execution context could effectively re-enter the VM between two ordinary statements. Therefore, in the current release, another task or event could observe the intermediate state between removing an array item and reinserting it.
A CriticalSection did not reliably solve this because the re-entry occurred on the same VM thread and the underlying lock was re-entrant. Thank you for pressing this point: my previous explanation was correct as a design goal, but not as a description of the implementation.
I've now corrected the scheduler comprehensively. This was a royal pain in the arse to fix! In the next release:
- Synchronous Objo code gets an uninterrupted turn.
Task.Run, task completions, timers and queued native events wait for a scheduler boundary.
- Multiple collection mutations within one synchronous method cannot be interleaved by another task or event.
Task.Yield now genuinely suspends execution until a later scheduler turn.
- A pending
Await remains a yield point, so assumptions about mutable state must still be revalidated after it.
- Queued UI control events obey the same rule, while preserving the correct event context:
Me is the event source and Self is the subscriber.
- Explicitly nested event loops created by
Window.ShowModal remain the documented exception, because showing a modal window must continue processing events.
Consequently, ordinary synchronous operations on an Array, Dictionary, shared field or other Objo state do not need a CriticalSection merely to prevent task or event re-entry. If a method removes an item and reinserts it before returning or awaiting, that whole sequence is now one uninterrupted turn.
I've also strengthened shutdown, exception propagation, debugger stopping and object-lifetime handling around these scheduler changes, and updated the async, task, critical-section and window documentation.
My internal benchmarks show no material VM performance regression. The per-instruction hot path remains essentially unchanged, with the new scheduling work occurring only at turn boundaries.
This will be included in the next release: https://feedback.objo.dev/bug/1364
Thanks again for questioning this so carefully. This was a real VM bug, and your example was what led me to find and correct it.