[foreign-memaccess] on confinement
Maurizio Cimadamore
maurizio.cimadamore at oracle.com
Tue Jun 4 11:34:12 UTC 2019
Hi,
as promised, here's a more detailed analysis on the state of confinement
in the foreign-memaccess branch.
The working theory, explained in [1] is that:
* all scopes have an owner thread
* all critical scope operation (close/fork/allocate) are confined to the
owner
* access to regions created by that scope is un-confined
This proposal was well received - but I now realized that it was flawed,
at least in part, and I think that this tension is now influencing some
of the discussions happening in [2]. So let's try to make some order.
Yes, this approach gives the scope owner some guarantees, as the scope
can never be closed behind the owner's back by some other thread. So far
so good. But a big reason for doing this, beside cleanliness of user
model is also another one: to avoid very costly synchronization whenever
memory is accessed.
I now realize that this goal is not met by the above proposal: that is,
confining scope critical operations is not enough to completely free us
from synchronization. Why? Well, while only the owner thread can close a
scope, if multiple threads have concurrent access to the same region
owned by the same scope - these threads would have to synchronize when
accessing memory, in order to make sure that memory is really alive at
the point of access; a failure to do so could result in accessing
already freed memory (= VM crash).
In other words, I believe the current implementation is still broken
when it comes to multi-threaded access to the same region (perhaps more
subtly so).
The only way to add multi threaded access w/o incurring in heavy
on-access synchronization cost is to make sure that, at any given point
in time, only one thread has access to a segment's memory. And, if this
thread is the only one that can perform 'close' operations, then we're
in the clear. There's really nothing bad that can happen in such a case.
So, of course if the only thing we wanted to support was confined
access, we could simply enforce confinement all the way down and be done
with it. But, in doing so, we would not be able to support all use cases
where some co-operation between multiple threads is required.
How do we add back some support for sharing a scope/segment across
multiple threads? This is where things get nasty:
1) segments starts off in a neutral state (no access) and to gain access
clients must perform an explicit 'acquire' operation (and then 'release'
to go back to the neutral state). This is, essentially, John's proposal
in [3].
2) segments starts off in a confined state (with respect to thread T).
But thread T is also allowed to transfer ownership to a different thread S.
I think that, while being more general, (1) has a lot more surface area
in the API/programming model. Now user have to become aware of this
state machine, where segments/scopes can go from neutral (John's call it
'queued') to owned, back to neutral. Note also that we already using
try-with-resources to allow for resource collection after use.
Implementing (1) would mean to add yet another orthogonal dimension -
that is, a segment/scope will support 'close' (to free memory),
'acquire' (to set ownership) and 'release' (to unset ownership). Also,
we have to worry about things like a thread calling 'acquire' 5 times,
but release only 4 times. In other words, I see that if we go down this
path, some kind of 'ownership counting' is probably unavoidable.
On the other hand, (2) is much simpler. A scope/segment only has a
single owner at any given point in time, but the twist is, that owner
can also transfer his powers to another thread (which will then become
the new owner). The obvious problem with this approach is that the
segment owner has to 'know' the thread is transferring ownership to -
which, I guess, could be too strict of an assumption in certain cases
(think about a pool of worker thread, where a master thread - the owner
of the original scope - might not necessarily know who's going to pick
up the pieces ahead of time).
And, worse, both (1) and (2) suffer from issues when it comes to
creating 'restricted views' of existing segments. Let's consider this
use case:
* thread A created a segment S
* thread A slices S into two sub-slices S1 and S2
* ownership of S1 is assigned to thread B, while ownership of S2 is
assigned to thread C (either using approach (1) or (2))
* when can thread A access S again?
The issue here is that, thanks to slicing, the root region S ends up
with _two_ owners at the same time (this can scale to the case of _n_
owners, of course). So things are quite more convoluted than (1) and (2)
can lead to imagine.
Here it would seem that (1) has an edge. After all, A can release
ownership, then slice S up into segments. Threads B and C can acquire S1
and S2 respectively. But this means that the acquire operation must have
a side-effect on the original region S too - that is, it has to
transition to an 'owned' state, so that A cannot acquire it again, at
least not until both B and C have released S1 and S2, respectively. Note
also that there's a time where thread A doesn't have ownership, but it
is still able to do operations on S (such as slicing it up into S1 and
S2) - which seems at odds with the 'only the owner can perform
segment/scope operation' rule. So, things are not as clean as they first
appear.
What about (2)? Surely a single-ownership mechanism cannot cope with a
de-facto multi-ownership scenario? Well, I think it can, although it's
not immediate to see. Let's see what the ownership transitions would be
like under (2):
* A starts off owning segment S
* A creates S1 and S2, which are both owned by A
* A transfers ownership of S1 to B - as a side-effect, this operation
effectively makes S a mixed ownership segment, so A has no longer access
to it (but note, A can still access S2!)
* A also transfer ownership of S2 to C. Now A doesn't have access to S
(see above) but also doesn't have access to S1 and S2 either.
* B terminates and transfer ownership of S1 back to A.
* C terminates and transfer ownership of S2 back to A.
* Now A can access both S1 and S2 - but if we're smart enough, we can
also detect that it has again access to the full S, which is again only
owned fully by A
So, it seems that, even with the complexity of (1), we still don't have
a clear cut way to think about the problem outlined above. And I'd
really really like to avoid exposing such complex state transitions into
the final API. I think the cross product of alive/dead state with
neutral/owned state leads to place where it's really hard to think about
who can do what at any given point in time (in addition to make the
implementation more convoluted and direct which might result in more
places for bugs to hide, as well as a potential for performance
degradation).
On the other hand, it seems like something like (2) would not only lead
to something more desirable API/programming model-wise, but also to a
cleaner path to supporting a multi-ownership scenario such as the one
described above. Whether the fact that (2) requires explicit ownership
transfers is too strict, is something that we don't have enough data
points, at this stage, to work with.
Of course I would have preferred to side-step all this and leave all
synchronization cost to the user - making only minimal assumptions; but,
as we have shown, this approach (which was my opening position!) has a
flaw in the sense that it leaves concurrent access exposed to the risk
of accessing a segment that's already been closed (which can result in a
VM crash). While this is an acceptable answer for an unsafe API, we
wouldn't want something like this to happen in a safe API.
Maurizio
[1] -
https://mail.openjdk.java.net/pipermail/panama-dev/2019-May/005494.html
[2] -
https://mail.openjdk.java.net/pipermail/panama-dev/2019-May/005644.html
[3] -
https://mail.openjdk.java.net/pipermail/panama-dev/2019-June/005651.html
More information about the panama-dev
mailing list