Implementing coroutines in Java

前端 未结 7 1747
你的背包
你的背包 2020-12-04 07:22

This question is related to my question on existing coroutine implementations in Java. If, as I suspect, it turns out that there is no full implementation of coroutines cur

相关标签:
7条回答
  • 2020-12-04 07:34

    There is also Quasar for Java and Project Loom at Oracle where extensions are made to the JVM for fibers and continuations. Here is a presentation of Loom on Youtoube. There are several more. Easy to find with a little searching.

    0 讨论(0)
  • 2020-12-04 07:48

    I would take a look at this: http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html, its pretty interesting and should provide a good place to start. But of course we are using Java so we can do better (or maybe worse because there are no macros :))

    From my understanding with coroutines you usually have a producer and a consumer coroutine (or at least this is the most common pattern). But semantically you don't want the producer to call the consumer or visa-versa because this introduces an asymmetry. But given the way stack based languages work we will need to have someone do the calling.

    So here is a very simple type hierarchy:

    public interface CoroutineProducer<T>
    {
        public T Produce();
        public boolean isDone();
    }
    
    public interface CoroutineConsumer<T>
    {
        public void Consume(T t);
    }
    
    public class CoroutineManager
    {
        public static Execute<T>(CoroutineProducer<T> prod, CoroutineConsumer<T> con)
        {
            while(!prod.IsDone()) // really simple
            {
                T d = prod.Produce();
                con.Consume(d);
            }
        }
    }
    

    Now of course the hard part is implementing the interfaces, in particular it is difficult to break a computation into individual steps. For this you would probably want a whole other set of persistent control structures. The basic idea is that we want to simulate non-local transfer of control (in the end its kinda like we're simulating a goto). We basically want to move away from using the stack and the pc (program-counter) by keeping the state of our current operations in the heap instead of on the stack. Therefore we are going to need a bunch of helper classes.

    For example:

    Let's say that in an ideal world you wanted to write a consumer that looked like this (psuedocode):

    boolean is_done;
    int other_state;
    while(!is_done)
    {
        //read input
        //parse input
        //yield input to coroutine
        //update is_done and other_state;
    }
    

    we need to abstract the local variable like is_doneand other_state and we need to abstract the while loop itself because our yield like operation is not going to be using the stack. So let's create a while loop abstraction and associated classes:

    enum WhileState {BREAK, CONTINUE, YIELD}
    abstract class WhileLoop<T>
    {
        private boolean is_done;
        public boolean isDone() { return is_done;}
        private T rval;
        public T getReturnValue() {return rval;} 
        protected void setReturnValue(T val)
        {
            rval = val;
        }
    
    
        public T loop()
        {
            while(true)
            {
                WhileState state = execute();
                if(state == WhileState.YIELD)
                    return getReturnValue();
                else if(state == WhileState.BREAK)
                        {
                           is_done = true;
                    return null;
                        }
            }
        }
        protected abstract WhileState execute();
    }
    

    The Basic trick here is to move local variables to be class variables and turn scope blocks into classes which gives us the ability to 're-enter' our 'loop' after yielding our return value.

    Now to implement our producer

    public class SampleProducer : CoroutineProducer<Object>
    {
        private WhileLoop<Object> loop;//our control structures become state!!
        public SampleProducer()
        {
            loop = new WhileLoop()
            {
                private int other_state;//our local variables become state of the control structure
                protected WhileState execute() 
                {
                    //this implements a single iteration of the loop
                    if(is_done) return WhileState.BREAK;
                    //read input
                    //parse input
                    Object calcluated_value = ...;
                    //update is_done, figure out if we want to continue
                    setReturnValue(calculated_value);
                    return WhileState.YIELD;
                }
            };
        }
        public Object Produce()
        {
            Object val = loop.loop();
            return val;
        }
        public boolean isDone()
        {
            //we are done when the loop has exited
            return loop.isDone();
        }
    }
    

    Similar tricks could be done for other basic control flow structures. You would ideally build up a library of these helper classes and then use them to implement these simple interfaces which would ultimately give you the semantics of co-routines. I'm sure everything I've written here can be generalized and expanded upon greatly.

    0 讨论(0)
  • 2020-12-04 07:53

    I'd suggest to look at Kotlin coroutines on JVM. It falls into a different category, though. There is no byte-code manipulation involved and it works on Android, too. However, you will have to write your coroutines in Kotlin. The upside is that Kotlin is designed for interoperability with Java in mind, so you can still continue to use all your Java libraries and freely combine Kotlin and Java code in the same project, even putting them side-by-side in the same directories and packages.

    This Guide to kotlinx.coroutines provides many more examples, while the coroutines design document explains all the motivation, use-cases and implementation details.

    0 讨论(0)
  • 2020-12-04 07:53

    I have a Coroutine class that I use in Java. It is based on threads and using threads has the advantage of allowing parallel operation, which on multicore machines can be an advantage. Therefore you might want to consider a thread based approach.

    0 讨论(0)
  • 2020-12-04 07:53

    There's an another choice is here for Java6+

    A pythonic coroutine implementation:

    import java.lang.ref.WeakReference;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.AtomicBoolean;
    import java.util.concurrent.atomic.AtomicReference;
    
    class CorRunRAII {
        private final List<WeakReference<? extends CorRun>> resources = new ArrayList<>();
    
        public CorRunRAII add(CorRun resource) {
            if (resource == null) {
                return this;
            }
            resources.add(new WeakReference<>(resource));
    
            return this;
        }
    
        public CorRunRAII addAll(List<? extends CorRun> arrayList) {
            if (arrayList == null) {
                return this;
            }
            for (CorRun corRun : arrayList) {
                add(corRun);
            }
    
            return this;
        }
    
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
    
            for (WeakReference<? extends CorRun> corRunWeakReference : resources) {
                CorRun corRun = corRunWeakReference.get();
                if (corRun != null) {
                    corRun.stop();
                }
            }
        }
    }
    
    class CorRunYieldReturn<ReceiveType, YieldReturnType> {
        public final AtomicReference<ReceiveType> receiveValue;
        public final LinkedBlockingDeque<AtomicReference<YieldReturnType>> yieldReturnValue;
    
        CorRunYieldReturn(AtomicReference<ReceiveType> receiveValue, LinkedBlockingDeque<AtomicReference<YieldReturnType>> yieldReturnValue) {
            this.receiveValue = receiveValue;
            this.yieldReturnValue = yieldReturnValue;
        }
    }
    
    interface CorRun<ReceiveType, YieldReturnType> extends Runnable, Callable<YieldReturnType> {
        boolean start();
        void stop();
        void stop(final Throwable throwable);
        boolean isStarted();
        boolean isEnded();
        Throwable getError();
    
        ReceiveType getReceiveValue();
        void setResultForOuter(YieldReturnType resultForOuter);
        YieldReturnType getResultForOuter();
    
        YieldReturnType receive(ReceiveType value);
        ReceiveType yield();
        ReceiveType yield(YieldReturnType value);
        <TargetReceiveType, TargetYieldReturnType> TargetYieldReturnType yieldFrom(final CorRun<TargetReceiveType, TargetYieldReturnType> another);
        <TargetReceiveType, TargetYieldReturnType> TargetYieldReturnType yieldFrom(final CorRun<TargetReceiveType, TargetYieldReturnType> another, final TargetReceiveType value);
    }
    
    abstract class CorRunSync<ReceiveType, YieldReturnType> implements CorRun<ReceiveType, YieldReturnType> {
    
        private ReceiveType receiveValue;
        public final List<WeakReference<CorRun>> potentialChildrenCoroutineList = new ArrayList<>();
    
        // Outside
    
        private AtomicBoolean isStarted = new AtomicBoolean(false);
        private AtomicBoolean isEnded = new AtomicBoolean(false);
        private Throwable error;
    
        private YieldReturnType resultForOuter;
    
        @Override
        public boolean start() {
    
            boolean isStarted = this.isStarted.getAndSet(true);
            if ((! isStarted)
                    && (! isEnded())) {
                receive(null);
            }
    
            return isStarted;
        }
    
        @Override
        public void stop() {
            stop(null);
        }
    
        @Override
        public void stop(Throwable throwable) {
            isEnded.set(true);
            if (throwable != null) {
                error = throwable;
            }
    
            for (WeakReference<CorRun> weakReference : potentialChildrenCoroutineList) {
                CorRun child = weakReference.get();
                if (child != null) {
                    child.stop();
                }
            }
        }
    
        @Override
        public boolean isStarted() {
            return isStarted.get();
        }
    
        @Override
        public boolean isEnded() {
            return isEnded.get();
        }
    
        @Override
        public Throwable getError() {
            return error;
        }
    
        @Override
        public ReceiveType getReceiveValue() {
            return receiveValue;
        }
    
        @Override
        public void setResultForOuter(YieldReturnType resultForOuter) {
            this.resultForOuter = resultForOuter;
        }
    
        @Override
        public YieldReturnType getResultForOuter() {
            return resultForOuter;
        }
    
        @Override
        public synchronized YieldReturnType receive(ReceiveType value) {
            receiveValue = value;
    
            run();
    
            return getResultForOuter();
        }
    
        @Override
        public ReceiveType yield() {
            return yield(null);
        }
    
        @Override
        public ReceiveType yield(YieldReturnType value) {
            resultForOuter = value;
            return receiveValue;
        }
    
        @Override
        public <TargetReceiveType, TargetYieldReturnType> TargetYieldReturnType yieldFrom(CorRun<TargetReceiveType, TargetYieldReturnType> another) {
            return yieldFrom(another, null);
        }
    
        @Override
        public <TargetReceiveType, TargetYieldReturnType> TargetYieldReturnType yieldFrom(CorRun<TargetReceiveType, TargetYieldReturnType> another, TargetReceiveType value) {
            if (another == null || another.isEnded()) {
                throw new RuntimeException("Call null or isEnded coroutine");
            }
    
            potentialChildrenCoroutineList.add(new WeakReference<CorRun>(another));
    
            synchronized (another) {
                boolean isStarted = another.start();
                boolean isJustStarting = ! isStarted;
                if (isJustStarting && another instanceof CorRunSync) {
                    return another.getResultForOuter();
                }
    
                return another.receive(value);
            }
        }
    
        @Override
        public void run() {
            try {
                this.call();
            }
            catch (Exception e) {
                e.printStackTrace();
    
                stop(e);
                return;
            }
        }
    }
    
    abstract class CorRunThread<ReceiveType, YieldReturnType> implements CorRun<ReceiveType, YieldReturnType> {
    
        private final ExecutorService childExecutorService = newExecutorService();
        private ExecutorService executingOnExecutorService;
    
        private static final CorRunYieldReturn DUMMY_COR_RUN_YIELD_RETURN = new CorRunYieldReturn(new AtomicReference<>(null), new LinkedBlockingDeque<AtomicReference>());
    
        private final CorRun<ReceiveType, YieldReturnType> self;
        public final List<WeakReference<CorRun>> potentialChildrenCoroutineList;
        private CorRunYieldReturn<ReceiveType, YieldReturnType> lastCorRunYieldReturn;
    
        private final LinkedBlockingDeque<CorRunYieldReturn<ReceiveType, YieldReturnType>> receiveQueue;
    
        // Outside
    
        private AtomicBoolean isStarted = new AtomicBoolean(false);
        private AtomicBoolean isEnded = new AtomicBoolean(false);
        private Future<YieldReturnType> future;
        private Throwable error;
    
        private final AtomicReference<YieldReturnType> resultForOuter = new AtomicReference<>();
    
        CorRunThread() {
            executingOnExecutorService = childExecutorService;
    
            receiveQueue = new LinkedBlockingDeque<>();
            potentialChildrenCoroutineList = new ArrayList<>();
    
            self = this;
        }
    
        @Override
        public void run() {
            try {
                self.call();
            }
            catch (Exception e) {
                stop(e);
                return;
            }
    
            stop();
        }
    
        @Override
        public abstract YieldReturnType call();
    
        @Override
        public boolean start() {
            return start(childExecutorService);
        }
    
        protected boolean start(ExecutorService executorService) {
            boolean isStarted = this.isStarted.getAndSet(true);
            if (!isStarted) {
                executingOnExecutorService = executorService;
                future = (Future<YieldReturnType>) executingOnExecutorService.submit((Runnable) self);
            }
            return isStarted;
        }
    
        @Override
        public void stop() {
            stop(null);
        }
    
        @Override
        public void stop(final Throwable throwable) {
            if (throwable != null) {
                error = throwable;
            }
            isEnded.set(true);
    
            returnYieldValue(null);
            // Do this for making sure the coroutine has checked isEnd() after getting a dummy value
            receiveQueue.offer(DUMMY_COR_RUN_YIELD_RETURN);
    
            for (WeakReference<CorRun> weakReference : potentialChildrenCoroutineList) {
                CorRun child = weakReference.get();
                if (child != null) {
                    if (child instanceof CorRunThread) {
                        ((CorRunThread)child).tryStop(childExecutorService);
                    }
                }
            }
    
            childExecutorService.shutdownNow();
        }
    
        protected void tryStop(ExecutorService executorService) {
            if (this.executingOnExecutorService == executorService) {
                stop();
            }
        }
    
        @Override
        public boolean isEnded() {
            return isEnded.get() || (
                    future != null && (future.isCancelled() || future.isDone())
                    );
        }
    
        @Override
        public boolean isStarted() {
            return isStarted.get();
        }
    
        public Future<YieldReturnType> getFuture() {
            return future;
        }
    
        @Override
        public Throwable getError() {
            return error;
        }
    
        @Override
        public void setResultForOuter(YieldReturnType resultForOuter) {
            this.resultForOuter.set(resultForOuter);
        }
    
        @Override
        public YieldReturnType getResultForOuter() {
            return this.resultForOuter.get();
        }
    
        @Override
        public YieldReturnType receive(ReceiveType value) {
    
            LinkedBlockingDeque<AtomicReference<YieldReturnType>> yieldReturnValue = new LinkedBlockingDeque<>();
    
            offerReceiveValue(value, yieldReturnValue);
    
            try {
                AtomicReference<YieldReturnType> takeValue = yieldReturnValue.take();
                return takeValue == null ? null : takeValue.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            return null;
        }
    
        @Override
        public ReceiveType yield() {
            return yield(null);
        }
    
        @Override
        public ReceiveType yield(final YieldReturnType value) {
            returnYieldValue(value);
    
            return getReceiveValue();
        }
    
        @Override
        public <TargetReceiveType, TargetYieldReturnType> TargetYieldReturnType yieldFrom(final CorRun<TargetReceiveType, TargetYieldReturnType> another) {
            return yieldFrom(another, null);
        }
    
        @Override
        public <TargetReceiveType, TargetYieldReturnType> TargetYieldReturnType yieldFrom(final CorRun<TargetReceiveType, TargetYieldReturnType> another, final TargetReceiveType value) {
            if (another == null || another.isEnded()) {
                throw new RuntimeException("Call null or isEnded coroutine");
            }
    
            boolean isStarted = false;
            potentialChildrenCoroutineList.add(new WeakReference<CorRun>(another));
    
            synchronized (another) {
                if (another instanceof CorRunThread) {
                    isStarted = ((CorRunThread)another).start(childExecutorService);
                }
                else {
                    isStarted = another.start();
                }
    
                boolean isJustStarting = ! isStarted;
                if (isJustStarting && another instanceof CorRunSync) {
                    return another.getResultForOuter();
                }
    
                TargetYieldReturnType send = another.receive(value);
                return send;
            }
        }
    
        @Override
        public ReceiveType getReceiveValue() {
    
            setLastCorRunYieldReturn(takeLastCorRunYieldReturn());
    
            return lastCorRunYieldReturn.receiveValue.get();
        }
    
        protected void returnYieldValue(final YieldReturnType value) {
            CorRunYieldReturn<ReceiveType, YieldReturnType> corRunYieldReturn = lastCorRunYieldReturn;
            if (corRunYieldReturn != null) {
                corRunYieldReturn.yieldReturnValue.offer(new AtomicReference<>(value));
            }
        }
    
        protected void offerReceiveValue(final ReceiveType value, LinkedBlockingDeque<AtomicReference<YieldReturnType>> yieldReturnValue) {
            receiveQueue.offer(new CorRunYieldReturn(new AtomicReference<>(value), yieldReturnValue));
        }
    
        protected CorRunYieldReturn<ReceiveType, YieldReturnType> takeLastCorRunYieldReturn() {
            try {
                return receiveQueue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            return null;
        }
    
        protected void setLastCorRunYieldReturn(CorRunYieldReturn<ReceiveType,YieldReturnType> lastCorRunYieldReturn) {
            this.lastCorRunYieldReturn = lastCorRunYieldReturn;
        }
    
        protected ExecutorService newExecutorService() {
            return Executors.newCachedThreadPool(getThreadFactory());
        }
    
        protected ThreadFactory getThreadFactory() {
            return new ThreadFactory() {
                @Override
                public Thread newThread(final Runnable runnable) {
                    Thread thread = new Thread(runnable);
                    thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                        @Override
                        public void uncaughtException(Thread thread, Throwable throwable) {
                            throwable.printStackTrace();
                            if (runnable instanceof CorRun) {
                                CorRun self = (CorRun) runnable;
                                self.stop(throwable);
                                thread.interrupt();
                            }
                        }
                    });
                    return thread;
                }
            };
        }
    }
    

    Now you can use pythonic coroutines in this way (e.g. fibonacci numbers)

    Thread Version:

    class Fib extends CorRunThread<Integer, Integer> {
    
        @Override
        public Integer call() {
            Integer times = getReceiveValue();
            do {
                int a = 1, b = 1;
                for (int i = 0; times != null && i < times; i++) {
                    int temp = a + b;
                    a = b;
                    b = temp;
                }
                // A pythonic "yield", i.e., it returns `a` to the caller and waits `times` value from the next caller
                times = yield(a);
            } while (! isEnded());
    
            setResultForOuter(Integer.MAX_VALUE);
            return getResultForOuter();
        }
    }
    
    class MainRun extends CorRunThread<String, String> {
    
        @Override
        public String call() {
    
            // The fib coroutine would be recycled by its parent
            // (no requirement to call its start() and stop() manually)
            // Otherwise, if you want to share its instance and start/stop it manually,
            // please start it before being called by yieldFrom() and stop it in the end.
            Fib fib = new Fib();
            String result = "";
            Integer current;
            int times = 10;
            for (int i = 0; i < times; i++) {
    
                // A pythonic "yield from", i.e., it calls fib with `i` parameter and waits for returned value as `current`
                current = yieldFrom(fib, i);
    
                if (fib.getError() != null) {
                    throw new RuntimeException(fib.getError());
                }
    
                if (current == null) {
                    continue;
                }
    
                if (i > 0) {
                    result += ",";
                }
                result += current;
    
            }
    
            setResultForOuter(result);
    
            return result;
        }
    }
    

    Sync(non-thread) version:

    class Fib extends CorRunSync<Integer, Integer> {
    
        @Override
        public Integer call() {
            Integer times = getReceiveValue();
    
            int a = 1, b = 1;
            for (int i = 0; times != null && i < times; i++) {
                int temp = a + b;
                a = b;
                b = temp;
            }
            yield(a);
    
            return getResultForOuter();
        }
    }
    
    class MainRun extends CorRunSync<String, String> {
    
        @Override
        public String call() {
    
            CorRun<Integer, Integer> fib = null;
            try {
                fib = new Fib();
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            String result = "";
            Integer current;
            int times = 10;
            for (int i = 0; i < times; i++) {
    
                current = yieldFrom(fib, i);
    
                if (fib.getError() != null) {
                    throw new RuntimeException(fib.getError());
                }
    
                if (current == null) {
                    continue;
                }
    
                if (i > 0) {
                    result += ",";
                }
                result += current;
            }
    
            stop();
            setResultForOuter(result);
    
            if (Utils.isEmpty(result)) {
                throw new RuntimeException("Error");
            }
    
            return result;
        }
    }
    

    Execution(Both versions will work):

    // Run the entry coroutine
    MainRun mainRun = new MainRun();
    mainRun.start();
    
    // Wait for mainRun ending for 5 seconds
    long startTimestamp = System.currentTimeMillis();
    while(!mainRun.isEnded()) {
        if (System.currentTimeMillis() - startTimestamp > TimeUnit.SECONDS.toMillis(5)) {
            throw new RuntimeException("Wait too much time");
        }
    }
    // The result should be "1,1,2,3,5,8,13,21,34,55"
    System.out.println(mainRun.getResultForOuter());
    
    0 讨论(0)
  • 2020-12-04 07:54

    I just came across this question and just want to mention that i think it might be possible to implement coroutines or generators in a similar way C# does. That said i don't actually use Java but the CIL has quite similar limitations as the JVM has.

    The yield statement in C# is a pure language feature and is not part of the CIL bytecode. The C# compiler just creates a hidden private class for each generator function. If you use the yield statement in a function it has to return an IEnumerator or an IEnumerable. The compiler "packs" your code into a statemachine-like class.

    The C# compiler might use some "goto's" in the generated code to make the conversion into a statemachine easier. I don't know the capabilities of Java bytecode and if there's something like a plain unconditional jump, but at "assembly level" it's usually possible.

    As already mentioned this feature has to be implemented in the compiler. Because i have only little knowledge about Java and it's compiler i can't tell if it's possible to alter / extend the compiler, maybe with a "preprocessor" or something.

    Personally i love coroutines. As a Unity games developer i use them quite often. Because i play alot of Minecraft with ComputerCraft i was curious why coroutines in Lua (LuaJ) are implemented with threads.

    0 讨论(0)
提交回复
热议问题