[foreign-memaccess] on confinement

Andrew Haley aph at redhat.com
Wed Jul 1 17:40:09 UTC 2020


Hi,

On 04/06/2019 12:34, Maurizio Cimadamore wrote:

> 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.

I've been working with Ron Pressler on his "Lifetime" class. The idea
is to register a lifetime in a try/finally block and then launch some
threads, either virtual or non-virtual. "non-virtual" is a name for
the kind of Java Threads we have today, "virtual Threads" are what
Project Loom now calls "fibers".

The idea of a Lifetime is that you create a Lifetime instance and then
launch some Threads:

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
    try (ExecutorService executor = Executors.newThreadExecutor(factory)) {
        writeSegment(segment, intHandle);  // Executed on this thread

        executor.submit(() -> {
            writeSegment(segment, intHandle);   // Executed on a newly-launched thread
        });
        // The newly-launched thread is guaranteed to have terminated so we
	// can close the MemorySegment.
    }
}

In order to make this work I've made some small modifications to
AbstractMemorySegmentImpl.java, in particular this:

--- a/src/jdk.incubator.foreign/share/classes/jdk/internal/foreign/AbstractMemorySegmentImpl.java
+++ b/src/jdk.incubator.foreign/share/classes/jdk/internal/foreign/AbstractMemorySegmentImpl.java
@@ -280,7 +290,7 @@ public abstract class AbstractMemorySegmentImpl implements MemorySegment, Memory
     }

     void checkRange(long offset, long length, boolean writeAccess) {
-        scope.checkValidState();
+        access.checkAccess();
         if (writeAccess && !isSet(WRITE)) {
             throw unsupportedAccessMode(WRITE);
         } else if (!writeAccess && !isSet(READ)) {

There's a new class called TemporalResource which is used to make sure
our gurantees are satisfied. access here is an instance of
TemporalResource.

Every call to TemporalResource::checkAccess() makes sure that the
parent of this thread is the thread that created the TemporalResource
and that we are still a child of the Lifetime in which the
TemporalResource was created. We can do this without any inter-thread
communication.

By hooking TemporalResources into Streams, we can also do things like

    try (MemorySegment segment = MemorySegment.allocateNative(SEGMENT_LENGTH*4)) {
        initialize(segment);
        System.out.println(StreamSupport.stream(new MySpliterator(segment), /* parallel */true).reduce(Integer::sum));
    }

This usage is secure because when we create a ReduceTask we do
this:

    ReduceTask(ReduceTask<P_IN, P_OUT, R, S> parent,
               Spliterator<P_IN> spliterator) {
        ...
        this.lifetime = parent.lifetime;
    }

    protected S doLeaf() {
        Lifetime old = JLA.unsafeSetLifetime(Thread.currentThread(), this.lifetime);
        try {
            return helper.wrapAndCopyInto(op.makeSink(), spliterator);
        } finally {
            JLA.unsafeSetLifetime(Thread.currentThread(), old);
        }
    }

And when we terminate a Stream we can ensure that we are still within
the Lifetime of the thread that created us.

I'm very sorry that all of this is rather hand-waving and I'm still
working on a stable and correct prototype, but I hope that you get the
idea, and that this is of interest to you.

-- 
Andrew Haley  (he/him)
Java Platform Lead Engineer
Red Hat UK Ltd. <https://www.redhat.com>
https://keybase.io/andrewhaley
EAC8 43EB D3EF DB98 CC77 2FAD A5CD 6035 332F A671



More information about the panama-dev mailing list