在 Java 中实现协程

这个问题和我在 Java 中现有的协同程序实现上的问题有关。如果正如我怀疑的那样,事实证明 Java 中目前还没有完整的协程实现,那么实现它们需要什么呢?

正如我在那个问题中所说,我知道以下几点:

  1. 您可以在后台将“协同程序”实现为线程/线程池。
  2. 您可以在后台使用 JVM 字节码做一些棘手的事情,使协程成为可能。
  3. 所谓的“达芬奇机器”JVM 实现有一些原语,使得协程在没有协程的情况下也可以执行 字节码操作。
  4. 还可能存在各种基于 JNI 的协程方法。

我将依次解决每个人的不足之处。

基于线程的协同程序

这种“解决方案”是病态的。协同程序的重点是 避免的线程、锁定、内核调度等开销。协同程序应该是轻巧快速的,并且只能在用户空间中执行。通过严格限制的全倾斜线程来实现它们可以摆脱所有的优势。

JVM 字节码操作

这个解决方案更实际,尽管有点难以实现。这大致相当于跳转到 C 语言的协同程序库的汇编语言(这就是它们中的许多工作原理) ,其优点是您只需要担心一个架构,并且正确处理这个问题。

它还使您只能在完全兼容的 JVM 堆栈上运行代码(这意味着,例如,没有 Android) ,除非您能够找到在不兼容的堆栈上执行相同操作的方法。但是,如果您确实找到了这样做的方法,那么现在您的系统复杂性和测试需求已经增加了一倍。

达芬奇机器

达芬奇机器是很酷的实验,但因为它不是一个标准的 JVM,它的功能将不会在任何地方可用。实际上,我怀疑大多数生产环境都会明确禁止使用达芬奇机器。因此,我可以用它来做一些很酷的实验,但不能用于任何我期望发布到现实世界的代码。

这也带来了与上面的 JVM 字节码操作解决方案类似的附加问题: 不能在替代堆栈(如 Android)上工作。

JNI 实现

这个解决方案使得在 Java 中这样做毫无意义。CPU 和操作系统的每一个组合都需要独立的测试,每一个都是潜在的令人沮丧的细微故障点。当然,我也可以把自己完全绑定在一个平台上,但这也使得用 Java 做事毫无意义。

那么..。

有没有办法在 Java 中实现协程而不使用这四种技术中的任何一种?或者我将被迫使用这四种方法中气味最小的一种(JVM 操作)来代替?


编辑补充:

只是为了确保包含混淆,这是一个 相关的问题到 另一个,但不一样。该公司正在寻求实施 存在,以避免不必要的重造轮子。这个问题涉及到,如果另一个问题无法回答,那么如何在 Java 中实现协程。目的是在不同的线程上保持不同的问题。

68282 次浏览

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.

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.

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.

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.

Kotlin uses the following approach for co-routines
(from https://kotlinlang.org/docs/reference/coroutines.html):

Coroutines are completely implemented through a compilation technique (no support from the VM or OS side is required), and suspension works through code transformation. Basically, every suspending function (optimizations may apply, but we'll not go into this here) is transformed to a state machine where states correspond to suspending calls. Right before a suspension, the next state is stored in a field of a compiler-generated class along with relevant local variables, etc. Upon resumption of that coroutine, local variables are restored and the state machine proceeds from the state right after suspension.

A suspended coroutine can be stored and passed around as an object that keeps its suspended state and locals. The type of such objects is Continuation, and the overall code transformation described here corresponds to the classical Continuation-passing style. Consequently, suspending functions take an extra parameter of type Continuation under the hood.

Check out the design document at https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md

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());

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.

Project Loom: https://jdk.java.net/loom/ introduce Continuations to Java. An example:

static final ContinuationScope scope=new ContinuationScope("TST");


public static void main(String[] args) {
example1();
}


// *********************************************************************
// *** EXAMPLE 1: Co-routine with three active phases:
// *********************************************************************


public static void example1() {
    

Continuation coroutine=new Continuation(scope,new Runnable() {
public void run() {
System.out.println("Part 1 - Statements");
Continuation.yield(scope); // DETACH 1
System.out.println("Part 2 - Statements");
Continuation.yield(scope); // DETACH 2
System.out.println("Part 3 - Statements");
}});
    

    

coroutine.run(); // Vil utføre Part 1.
System.out.println("Returns here after first DETACH(Yield)");
coroutine.run(); // Vil utføre Part 2.
System.out.println("Returns here after second DETACH(Yield)");
coroutine.run(); // Vil utføre Part 3.
System.out.println("Returns here after 'FINAL END'");
System.out.println("Next line should be: IllegalStateException: Continuation terminated");
coroutine.run(); // IllegalStateException: Continuation terminated
}

Instead of using any other method just create a wrapper class for java

/**
* This class will be used run java code in the kotlin coroutines
* @author : prustyA : 17/06/2022
*/
class CoroutineJava {


//Scope
private val context: CoroutineContext = Dispatchers.IO
private val scope = CoroutineScope(context)


/**
* This method will be used to return current coroutine context
* @author : prustyA : 17/06/2022
*/
fun getContext() = context


/**
* This method will be used to start executing the method block
* @author : prustyA : 17/06/2022
*/
fun launch(block: () -> Unit) {
scope.launch { block() }
}


/**
* This method will be used to change the context and run the block
* @author : prustyA : 17/06/2022
*/
fun launchWithContext(context: CoroutineContext,block: () -> Unit) {
scope.launch {
withContext(context) { block() }
}
}
}