3 ms·
> Could be wrong, but the JEP feels like it goes a bit deeper than `async/await` in C#. It feels like a hybrid between Task and Channels but operating at a lowe
by neonsunset 2y ago
> Could be wrong, but the JEP feels like it goes a bit deeper than `async/await` in C#. It feels like a hybrid between Task and Channels but operating at a lower level.
Reading the proposal again, it's difficult to agree with this. The proposal talks about concepts that occur naturally when writing asynchronous code with C# (or F#, which has its own pros and cons, but mostly nicer combinators).
For example:
Response handle() throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> user = scope.fork(() -> findUser());
Supplier<Integer> order = scope.fork(() -> fetchOrder());
scope.join() // Join both subtasks
.throwIfFailed(); // ... and propagate errors
// Here, both subtasks have succeeded, so compose their results
return new Response(user.get(), order.get());
}
}
becomes the following in C#
async Task<Response> Handle()
{
var user = FindUser();
var order = FindOrder();
return new(await user, await order);
}
For the happy path, the difference can be more drastic
<T> List<T> runAll(List<Callable<T>> tasks)
throws InterruptedException, ExecutionException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
List<? extends Supplier<T>> suppliers = tasks.stream().map(scope::fork).toList();
scope.join()
.throwIfFailed(); // Propagate exception if any subtask fails
// Here, all tasks have succeeded, so compose their results
return suppliers.stream().map(Supplier::get).toList();
}
turns into
var results = await Task.WhenAll(tasks);
// Or, assuming .GetUser returns Task<GitHubUser>
var names = new[] { "java", "dotnet", "golang", "rust-lang", "python" };
var repos = await Task.WhenAll(
names.Select(n => GitHubApi.GetUser(n));
And so on and so forth. You can mix and match more complicated flows with CancellationToken and its CancellationTokenSource, TaskCompletionSource, Task.WhenAll/Any/Each (the last one transforms a collection of tasks into an asynchronous sequence of results). Apply exception handling at per-task or task collection level for additional customization. All of this occurs by composing the building blocks, rather than needing complex and bespoke API.
- CharlieDigital 2y agoI still don't think it's an exact analogue and has more of a flavor of System.Threading.Channels. In particular, when you look at the proposal for fan-in.
- neonsunset 2y agoFan-in, as described in the proposal: void serve(ServerSocket serverSocket) throws IOException, InterruptedException { try (var scope = new StructuredTaskScope<Void>()) { try { while (true) { var socket = serverSocket.accept(); scope.fork(() -> handle(socket)); } } finally { // If there's been an error or we're interrupted, we stop accepting scope.shutdown(); // Close all active connections scope.join(); } } } It's not exactly clear the exact specifics of how the above works regarding child tasks being able or unable to shutdown the parent scope - it only has the exception handler for the loop itself, which then performs .shutdown, which interrupts all the child tasks. However, if we continue with this assumption, then C# implementation is just the following: async Task Serve(Socket socket) { using var cts = new CancellationTokenSource(); try { while (true) { var conn = await socket.AcceptAsync(cts.Token); // Or pass cts to Handle itself if you want to allow the handler to cancel the listener task. _ = Task.Run(() => Handle(conn, cts.Token)); } } finally { cts.Cancel(); } } With that said, the difference to Java Structured Concurrency as proposed is that it offers tree-like tracking of child tasks/green threads, control over their execution, and, possibly, nicer debugging experience. I don't have an opinion whether the semantics of .shutdown() which ends up calling .interrupt() are sound without reading more carefully. If it can interrupt the execution at any point, this is extremely dangerous and will break a lot of existing code. It's one of the reasons why thread.Abort() got deprecated in .NET - the API brings too many issues and cannot be made to work reliably without hindering optimizations or violating expectations. If it's not the case and it's more like flowing CancellationToken implicitly, then it may be a nicer experience depending on the situation. If it only interrupts upon suspending on .Join, IO wait or similar - it's still the kind of API that you are forced into instead of deciding whether cooperative cancellation is even safely possible given specific logic, without having to write try-catch blocks for each callsite. This is my surface impression and I will continue looking into this API.