-
Notifications
You must be signed in to change notification settings - Fork 465
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
Upgraded Timer
API and fix Timer
tests...
#323
Conversation
The timer now takes a `Box<F>` where F implements a closure w/ the signature `Fn() -> u32+Send` The timer starts as soon as it is created, and ends under the following conditions: * The callback, when invoked, returns a non-positive continuation interval * The user drops the `Timer`, either by calling `drop(timer)` or by letting it go out of scope. The timer is cancelled when exiting scope to ensure memory-safety. Since the timer owns the closure's environment: the closure cannot safely be called once the timer itself has been freed. --- The `Send` bound implies that closures must be able to move to another thread. This is because SDL does not guarantee that timers will be run on the current thread. This means mutable access to data will have to be done in a thread-safe manner using the primitives in `std::sync`. --- * The tests have been updated to reflect the new API. * They have also been made more robust to intermittent failure. The offending test now has an upper limit, after which it will cancel the timer. * This is meant to ensure the mutex will be available for locking when the test checks the exit condition.
1. Remove unnecessary return 2. Use `u32` instead of `usize`
4df1e69
to
e5aa71d
Compare
I would only make two changes:
I think
Which leads to I've said contradicting things in #312, but I didn't fully understand |
I didn't catch it before, but why is there a generic type |
I was going off of the Concurrency chapter of the Rust guide which has the following to say about
The closure has a single owner ( As for EDIT: Note that |
The timer owns the callback ( EDIT: The |
Consider this scenario: The closure captures an Even though the closure itself is not being called simultaneously across multiple threads, the values within could be. In the case of |
I think the A generic type for let timers: Vec<Timer>; |
This doesn't work because There is no way to pass two pointers to SDL2's timer callback. You also can't pass a pointer to the trait-object because the trait-object moves wherever the Timer does. (Which means you need to go back to having a Guard that prevents the Timer from moving while active; or you need a way to fix-up the SDL callback every time the Timer moves.)
The
You can't put The entire environment of the closure doesn't need to be It is my understanding that those types which provide safe concurrent access are supposed to be the primary users of the |
The It's possible to have types that are |
On second thought, I might be wrong about Why are threads so confusing? |
Usually, closures capture environment variables by reference. A But fundamentally, even with a |
I'm unsure, but this might solve the problem with transmuting the
EDIT: Nope: the box is just a plain old trait object. |
You're right in saying the closure's environment never moves. It is always in the same spot once However I don't think dropping the
Send is simply defined as:
I believe you might be thinking of this line from the Sync docs instead?
It doesn't unfortunately. (I already tried it.) You can't pass the whole thing to SDL by value (it doesn't fit in 1-word.) If you pass it by reference: that reference is invalidated whenever the Timer (which owns the closure) moves. It's the exact same problem as passing the trait object unfortunately. In order to use closures w/o the generic bound you would need to store them in statically allocated storage, which adds a bunch of allocation & book-keeping. Then the Timer wouldn't need to own the closure, it would simply have some sort of reference to it when it needs to be freed or reclaimed. |
Again you can make that sort of API work, but it's cumbersome to use. The closure either has to outlive the timer, which greatly complicates how you can store the Timer anyways, or you need RAII guards so the timer stops when it is moved. |
Here is a concrete example of why you need a guard for that sort of API, and also how cumbersome it is to use.
|
819cb61
to
6ed205a
Compare
The trait object itself is stored on the heap, a 1-word pointer to this trait object is then passed to SDL and used to fire the callback. Implies the Sync+'static bound to guarantee that the closures envrionment can be invoked safely from another thread.
6ed205a
to
10c2a82
Compare
How about this? It gets rid of the generic parameter What we have here is
That being said: it passes my existing tests ... and there should be no reason you can't store it in a heterogeneous collection of timers.
|
Sorry for being so annoying, BTW. To be fair, we're dealing with both concurrent programming and C interop. 😵 It's annoying that double indirection is necessary to keep the generic |
Sadly I can't think of any other way to do it, we're very limited by the SDL API here. We have to cram our callback into one pointer. (Plus that pointer needs to live long enough if we want memory safety!) I am liking the current API though. It requires the Timer to have a lifetime parameter, but what's cool is you can just store a (... and if you try to store a As for the Regardless we can always relax the trait bounds w/o breaking existing code, tightening the bounds is much more difficult. |
You're an absolute star |
Upgraded `Timer` API and fix `Timer` tests...
This PR is based on the feedback from issue #320 ... it introduces a more Rust-like interface for the Timer wrapper that is much less error-prone, and also memory safe!
To summarize how it works:
boxed
closure to the timer, e.g:let t = Timer::new(Box::new(move|| { ... }));
invocations would point to freed memory.
This also improves thread safety over the previous API(s), from the SDL2 documentation:
Since the callback might be run on a separate thread: the closure's environment must satisfy
Send
so that it can be moved to the thread the timer will run on.Knowing this: it would also be unsafe to allow the closure to freely mutate its environment, so the closure does not allow for a mutable environment. In practice this simply means you have to protect mutable data w/ a
Mutex
orRwLock
, and shared data w/ anArc
.The tests have been improved so they hopefully don't fail spuriously anymore. I accomplished this by simply adding an upper-bound to the timer's callback so it stops trying to acquire the mutex after a certain number of iterations.
The whole thing is marked
unstable
for a number of reasons:Box<T>
we mayneedwant to change the API based on how the placement-new syntax (#![feature(box)]
) pans out in rust-1.0.0Box<T>
themselves?