Skip to content

Virtual threads#65

Merged
Brutus5000 merged 19 commits into
FAForever:masterfrom
GodFuper:feature/virtual-threads
Dec 5, 2024
Merged

Virtual threads#65
Brutus5000 merged 19 commits into
FAForever:masterfrom
GodFuper:feature/virtual-threads

Conversation

@GodFuper
Copy link
Copy Markdown
Contributor

@GodFuper GodFuper commented Nov 30, 2024

Virtualthreads don't like "synchronized". Therefore, I changed it to Lock.
Now all Threads are executed in Executor. And this Executor runs on virtual threads.

Switching to virtual threads requires 21 java.

Thread.sleep(20);
} catch (InterruptedException e) {
log.error(getLogPrefix() + "Interrupted while waiting for ICE", e);
onConnectionLost();
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it on my own. I think if interrupted, then it is possible to terminate the connection.

return socket;
} catch (SocketException e) {
log.error("Could not create socket for peer: {}", getPeerIdentifier(), e);
throw e;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the socket has returned an error, then it doesn't make sense further. Because we are going to get a LocalPort, and the socket will be null.

@Brutus5000 Brutus5000 self-requested a review November 30, 2024 20:27
Comment thread ice-adapter/src/main/java/com/faforever/iceadapter/AsyncService.java Outdated
Comment thread ice-adapter/src/main/java/com/faforever/iceadapter/AsyncService.java Outdated
Comment thread ice-adapter/src/main/java/com/faforever/iceadapter/AsyncService.java Outdated
Comment thread ice-adapter/src/main/java/com/faforever/iceadapter/AsyncService.java Outdated

public void close() {
running = false;
refreshThread.interrupt();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove? If one of the methods in the while loop are blocking, the thread will keep being blocked.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, it will be blocked until the game ends. I returned it.

Copy link
Copy Markdown
Member

@Brutus5000 Brutus5000 Nov 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes me wonder though, if then the running flag even makes sense 🤔

Comment thread ice-adapter/src/main/java/com/faforever/iceadapter/AsyncService.java Outdated
# Conflicts:
#	ice-adapter/src/main/java/com/faforever/iceadapter/IceAdapter.java
#	ice-adapter/src/main/java/com/faforever/iceadapter/debug/DebugWindow.java
#	ice-adapter/src/main/java/com/faforever/iceadapter/debug/DebugWindowController.java
#	ice-adapter/src/main/java/com/faforever/iceadapter/gpgnet/GPGNetServer.java
#	ice-adapter/src/main/java/com/faforever/iceadapter/ice/Peer.java
#	ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerIceModule.java
#	ice-adapter/src/main/java/com/faforever/iceadapter/rpc/RPCHandler.java
Comment thread ice-adapter/src/main/java/com/faforever/iceadapter/IceAdapter.java
Copy link
Copy Markdown
Member

@Brutus5000 Brutus5000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am just wondering if we don't shoot ourselves in the foot here.

From my understanding VirtualThreads were created to simplify the handling of asynchronous code. So using CompletableFuture.async methods handled in a one virtual thread per task-executor is the backwards compatible way to do it, but is there any benefit for new code to do so?

Comment on lines +77 to +82
CompletableFuture.runAsync(
() -> {
Thread.currentThread().setName("sendingLoop");
sendingLoop();
},
executor);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels dangerous. If you don't pass a OneNewVirtualThreadPerTask executor, you might rename and block a shared pool thread.

Since the thread is a loop and not a one time task, why don't just spawn a virtual thread here and leave out the completable future?


listenerThread = new Thread(this::listenerThread);
listenerThread.start();
listener = CompletableFuture.runAsync(this::listenerThread, IceAdapter.getExecutor());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Why not just spawn a virtual thread directly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You think that if I do this: "Thread.ofVirtual().start(this::listenerThread)". Will something change?

We are setting this up at the executor level.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a reference to the thread so you can set the name, exceptionHandler and interrupt it.

Comment on lines +58 to +64
checker = CompletableFuture.runAsync(
() -> {
Thread.currentThread().setName(getThreadName());
checkerThread();
},
IceAdapter.getExecutor());
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a continuous task too, so just spawning a virtual thread seems easer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

executor = Executors.newVirtualThreadPerTaskExecutor();

This means that all tasks where there is an executor will be on virtual threads.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The executor is not defined in this class. So you couple this class to the implementation of another class.
But apart from that e don't make any use of the completablefuture. Since we don't run any follow-up operations on the future, just Thread.ofVirtual().name(...).uncaugtExceptionHandler(...) is easier to understand.


listenerThread = new Thread(this::listener);
listenerThread.start();
listener = CompletableFuture.runAsync(this::listener, IceAdapter.getExecutor());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would launch a virtual thread here too.

@Sheikah45
Copy link
Copy Markdown
Member

Sheikah45 commented Dec 1, 2024

I am just wondering if we don't shoot ourselves in the foot here.

From my understanding VirtualThreads were created to simplify the handling of asynchronous code. So using CompletableFuture.async methods handled in a one virtual thread per task-executor is the backwards compatible way to do it, but is there any benefit for new code to do so?

I think using an executor is still probably the right choice as that way in the future if we find out that in some places virtual threads are not the right choice or additional execution considerations need to be added we can easily do that by swapping out the executor.

Although I do see your point on using the CompletableFuture API for infinite loop tasks and one shot / fire and forget tasks. In those cases I agree it might be best to just use the executor directly with executor.submit etc.

@GodFuper
Copy link
Copy Markdown
Contributor Author

GodFuper commented Dec 3, 2024

I am just wondering if we don't shoot ourselves in the foot here.
From my understanding VirtualThreads were created to simplify the handling of asynchronous code. So using CompletableFuture.async methods handled in a one virtual thread per task-executor is the backwards compatible way to do it, but is there any benefit for new code to do so?

I think using an executor is still probably the right choice as that way in the future if we find out that in some places virtual threads are not the right choice or additional execution considerations need to be added we can easily do that by swapping out the executor.

Although I do see your point on using the CompletableFuture API for infinite loop tasks and one shot / fire and forget tasks. In those cases I agree it might be best to just use the executor directly with executor.submit etc.

And what is the difference between executor.submit and CompletableFuture.RunAsync(... , executor) ? We get the "Future" there and there.

@Sheikah45
Copy link
Copy Markdown
Member

The main difference is the simplified API. Since only a normal future is returned it doesn't allow you to perform the async callback operations making it clear that the submitted task should be fairly standalone.

@Brutus5000
Copy link
Copy Markdown
Member

The main difference is the simplified API. Since only a normal future is returned it doesn't allow you to perform the async callback operations making it clear that the submitted task should be fairly standalone.

That would not be my interpretation of reading such a signature.
And looking at the overall code quality of this project we are discussing on the wrong level. It just doesn't matter right now which API we use. Everything is better than what we currently have.

@GodFuper
Copy link
Copy Markdown
Contributor Author

GodFuper commented Dec 3, 2024

To the places where was "the thread = new Thread(...)" I returned the previous logic. In places where Thread was not controlled from close(), it will now be controlled from Executor.

I suggest 2 options for controlling Threads:

  1. The "parent" class must controlhim Threads and close threads when object.close()
  2. CompletableFuture is controlled using executor and closed using executor.shutdown().

@UtilityClass
public class ExecutorHolder {
public ExecutorService getExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This creates a new executor every time. I guess we should keep one instance and return that all the time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private final ExecutorService executor = ExecutorHolder.getExecutor();
We create it once in the main class, and then distribute it to all other classes via IceAdapter.getExecutor()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I guess that's not what Sheikah had in mind, but it's fine for now.

Comment on lines +110 to +111
} catch (InterruptedException e) {
log.info("Sending loop interrupted");
Copy link
Copy Markdown
Member

@Brutus5000 Brutus5000 Dec 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catching the interrupt without re-interrupting will clear out the .isInterrupted() state and block the while loop from ending

https://chatgpt.com/share/67502077-b55c-800e-99d3-f6ede4960126

Copy link
Copy Markdown
Contributor Author

@GodFuper GodFuper Dec 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would be right if the code had while (true).
But we have the use of while (!Thread.currentThread().isInterrupted()). After that, the 'while' ends and the thread stop doing work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you catch the InterruptedException, !Thread.isInterrupted() returns always true.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right, I forgot that this mechanism works like that. I'll fix it in the code now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants