You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/10/13 22:05:45 UTC
(groovy) branch master updated: GPars migration doc enhancements
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 13287d542a GPars migration doc enhancements
13287d542a is described below
commit 13287d542a08d03007d6f03c3c93a9a88fbceba3
Author: Paul King <pa...@asert.com.au>
AuthorDate: Wed Oct 14 08:05:33 2026 +1000
GPars migration doc enhancements
---
src/spec/doc/_core-async-channels.adoc | 79 ++++++++++
src/spec/doc/_core-gpars-migration.adoc | 227 ++++++++++++++++++++++++++++
src/spec/doc/_design-pattern-decorator.adoc | 2 +-
src/spec/doc/core-async-await.adoc | 2 +
src/spec/test/AsyncAwaitSpecTest.groovy | 31 ++++
5 files changed, 340 insertions(+), 1 deletion(-)
diff --git a/src/spec/doc/_core-async-channels.adoc b/src/spec/doc/_core-async-channels.adoc
index 4873dbaa83..db2aa310d9 100644
--- a/src/spec/doc/_core-async-channels.adoc
+++ b/src/spec/doc/_core-async-channels.adoc
@@ -401,3 +401,82 @@ through the same single-threaded delivery path as `onNext` /
`onComplete`, preserving §1.3 signal serialization; subscribers do
not see those signals racing on the caller's thread.
====
+
+[[async-channels-csp]]
+== Coming from JCSP or GroovyCSP
+
+Channels, select, preconditions and timer branches give `groovy.concurrent`
+the core vocabulary of CSP, so process networks written with
+https://www.cs.kent.ac.uk/projects/ofa/jcsp/[JCSP], or with the GroovyCSP
+helpers (`PAR`, `ALT`) that shipped in GPars, translate shape for shape.
+A process becomes an `async` task, a `PAR` becomes a scope, and the poison
+pill that shuts a network down becomes `close()`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=csp_network,indent=0]
+----
+
+Each stage reads until its input is closed and drained, then closes its own
+output, so shutdown flows down the network just as poison does.
+`AsyncScope.withScope` does not return until every process in it has
+finished, and a failing process cancels the rest.
+
+[cols="2,3", options="header"]
+|===
+| JCSP / GroovyCSP | `groovy.concurrent`
+
+| `Channel.one2one()` (unbuffered)
+| `AsyncChannel.create()`, a rendezvous channel: a send waits for its receive
+
+| `Channel.one2one(new Buffer(n))`
+| `AsyncChannel.create(n)`
+
+| `any2one`, `one2any`, `any2any`
+| Any `AsyncChannel`. Channel ends are not owned, so any task may send or receive
+
+| `out.write(v)` / `in.read()`
+| `await c.send(v)` / `await c.receive()`, or `for await (v in c)` to read until closed
+
+| A `CSProcess`
+| An `async { }` task, or a method that returns one
+
+| `PAR`
+| `AsyncScope.withScope { scope -> scope.async { … } … }`
+
+| Poison
+| `close()`. Receivers drain what was sent, then see the channel as closed
+
+| `GDelta` and other copy-to-every-output processes
+| `BroadcastChannel` and `subscribe()`, or `tap` for a single copy
+
+| `ALT` with `priSelect()`
+| `ChannelSelect.from(a, b).select()`, which prefers channels in list order
+
+| `ALT` with `fairSelect()`
+| `ChannelSelect.from(a, b).fair()`, built once and kept across calls
+
+| `ALT` with `select()`, an arbitrary choice
+| `random()`
+
+| `select(boolean[] preCondition)`
+| `receive(c).when { … }` on each offer, or `select(boolean... enabled)`
+
+| `CSTimer` as an `ALT` guard
+| `ChannelSelect.after(millis)` for a timeout that re-arms every round,
+ or `AsyncChannel.after(millis)` for one fixed deadline
+
+| Output guards (sending inside an `ALT`)
+| `offers(send(c, v), receive(d))`
+
+| `Barrier` (`sync`, `enroll`, `resign`)
+| `java.util.concurrent.Phaser` (`arriveAndAwaitAdvance`, `register`, `arriveAndDeregister`)
+
+| `Crew` (concurrent read, exclusive write)
+| `@WithReadLock` / `@WithWriteLock`
+|===
+
+The rendezvous default matters when porting: a JCSP network that relies on
+unbuffered channels for synchronisation should use `AsyncChannel.create()`,
+not a buffered channel, or a producer will run ahead of its consumer.
+JCSP's networked and mobile channels have no counterpart.
diff --git a/src/spec/doc/_core-gpars-migration.adoc b/src/spec/doc/_core-gpars-migration.adoc
new file mode 100644
index 0000000000..83d08615f5
--- /dev/null
+++ b/src/spec/doc/_core-gpars-migration.adoc
@@ -0,0 +1,227 @@
+//////////////////////////////////////////
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+//////////////////////////////////////////
+
+[[gpars-migration]]
+== Coming from GPars
+
+Before Groovy 6, http://gpars.org/[GPars] was the usual library for
+parallel collections, actors, agents and dataflow in Groovy. The
+`groovy.concurrent` package covers most of the same ground, rebuilt around
+virtual threads, structured concurrency and `async`/`await`. GPars still
+works with Groovy 6, so existing code can keep using it and move over one
+feature at a time, but new code rarely needs it. The tables below map each
+GPars construct to its Groovy 6 counterpart.
+
+The biggest change in style is that results are `Awaitable` values rather
+than GPars `Promise` or `Future` objects, and that you `await` them instead
+of calling `get()` or `getVal()`. `await` also accepts any
+`java.util.concurrent.Future`, including the one GPars' `callAsync` returns.
+
+[[gpars-migration-parallel]]
+=== Parallel collections
+
+[cols="2,3", options="header"]
+|===
+| GPars | Groovy 6
+
+| `GParsPool.withPool { … }`, `withPool(n) { … }`
+| `ParallelScope.withPool(Pool.cpu()) { scope -> … }`, `withPool(n) { scope -> … }`.
+ Parallel methods also work outside any scope, on the common pool
+
+| `withExistingPool(pool) { … }`
+| `ParallelScope.withPool(pool) { scope -> … }` with a `Pool`
+
+| `eachParallel`, `eachWithIndexParallel`, `collectParallel`,
+ `collectManyParallel`, `findAllParallel`, `findParallel`,
+ `findAnyParallel`, `grepParallel`, `splitParallel`, `anyParallel`,
+ `everyParallel`, `countParallel`, `groupByParallel`, `injectParallel`
+| Same names, on any `Collection`
+
+| `sumParallel()`
+| `sumParallel { a, b -> a + b }`. The accumulator is required
+
+| `minParallel { it.age }`, `maxParallel()`
+| `minParallel { a, b -> a.age <=> b.age }`. A two-argument comparator is required
+
+| `foldParallel`
+| `injectParallel`
+
+| `makeConcurrent()`, `asConcurrent { }`, `ParallelEnhancer`
+| Call the `…Parallel` methods directly
+
+| `collection.parallel` (`map`, `filter`, `reduce` on a parallel array)
+| `parallelStream()`, or chain the `…Parallel` methods
+
+| `GParsExecutorsPool`
+| `Pool.io()` or `Pool.virtual()`
+
+| `executeAsync(…)`, `executeAsyncAndWait(…)`
+| `async { }` per task, then `await Awaitable.all(…)`
+
+| `speculate(…)`
+| `await Awaitable.first(…)`, the first to succeed
+
+| `runForkJoin(…)`
+| `java.util.concurrent.RecursiveTask` on a `ForkJoinPool`
+|===
+
+GPars added its parallel methods to maps, strings and other iterable
+objects as well. Groovy 6 adds them to `Collection` only, so call
+`entrySet()`, `toList()` or similar first.
+
+The closures passed to the Groovy 6 methods are converted to
+`java.util.function` types, so their parameter count is fixed. For example,
+`minParallel` and `maxParallel` take a two-argument comparator and do not
+accept GPars' one-argument form.
+
+[[gpars-migration-async]]
+=== Asynchronous functions
+
+[cols="2,3", options="header"]
+|===
+| GPars | Groovy 6
+
+| `closure.callAsync(args)`
+| `async { closure(args) }`
+
+| `closure.asyncFun()`
+| A closure or method that returns `async { … }`. See
+ <<async-closure-factories,Async Closure Factories>>
+
+| `Promise.then { … }`
+| `awaitable.then { … }`, or plain sequential code after `await`
+
+| `Dataflow.whenAllBound(a, b) { x, y -> … }`
+| `def (x, y) = await(a, b)`
+|===
+
+[[gpars-migration-agents-actors]]
+=== Agents and actors
+
+[cols="2,3", options="header"]
+|===
+| GPars | Groovy 6
+
+| `new Agent(0)`
+| `Agent.create(0)`
+
+| `agent << { updateValue(it + 1) }`
+| `agent.send { it + 1 }`. The closure returns the new value
+
+| `agent.val`, `agent.valAsync { … }`
+| `await agent.getAsync()` waits for pending updates; `agent.get()` reads at once
+
+| `agent.sendAndWait { … }`
+| `await agent.sendAndGet { … }`
+
+| `agent.addListener { oldValue, newValue -> … }`
+| `for await (v in agent.changes())`, or any `Flow.Subscriber`
+
+| `Actors.reactor { it * 2 }`
+| `Actor.reactor { it * 2 }`
+
+| `Actors.actor { loop { react { msg -> … } } }` holding state in local variables
+| `Actor.stateful(initial) { state, msg -> … }`, returning the new state
+
+| `Actors.messageHandler { when { String s -> … } }`, `DynamicDispatchActor`
+| A `switch` on the message inside `Actor.reactor` or `Actor.stateful`
+
+| `actor << msg`, `actor.send(msg)`
+| `actor.send(msg)`
+
+| `actor.sendAndWait(msg)`
+| `await actor.sendAndGet(msg)`
+
+| `actor.sendAndContinue(msg) { reply -> … }`
+| `actor.sendAndGet(msg).then { reply -> … }`
+
+| Nested `react` calls that change what the actor accepts next
+| `ctx.become(handler)`, `ctx.stash()` and `ctx.unstashAll()` with the `ActorContext` handler forms
+
+| `@groovyx.gpars.activeobject.ActiveObject`, `@ActiveMethod`
+| `@groovy.transform.ActiveObject`, `@groovy.transform.ActiveMethod`.
+ `blocking` defaults to `true` in both
+
+| `DefaultPGroup`, `actor.parallelGroup = group`
+| `ActorOptions.DEFAULTS.withExecutor(pool)`
+|===
+
+[[gpars-migration-dataflow]]
+=== Dataflow
+
+[cols="2,3", options="header"]
+|===
+| GPars | Groovy 6
+
+| `Dataflow.task { … }`
+| `async { … }`
+
+| `DataflowVariable`, `v << value`
+| `groovy.concurrent.DataflowVariable`, `v << value`
+
+| `v.val`, `v.get()`
+| `await v`
+
+| `v.whenBound { … }`, `v.then { … }`
+| `v.then { … }`
+
+| `v.bindError(e)`
+| `v.bindError(e)`
+
+| `Dataflows`
+| `groovy.concurrent.Dataflows`
+
+| `DataflowQueue`
+| `AsyncChannel.create(n)`. Buffers are bounded, where `DataflowQueue` is not
+
+| `SyncDataflowQueue`
+| `AsyncChannel.create()`, a rendezvous channel
+
+| `DataflowBroadcast`, `createReadChannel()`
+| `BroadcastChannel`, `subscribe()`
+
+| `Dataflow.select(a, b)`
+| `ChannelSelect.from(a, b)`
+
+| `PoisonPill`
+| `close()`
+
+| `Dataflow.operator(…)`, `selector(…)`, `splitter(…)`, and channel methods such as `chainWith`, `filter` and `tap`
+| An `async` loop over input channels, or `map`, `filter`, `merge`, `split` and `tap` on `AsyncChannel`
+|===
+
+GPars' CSP helpers (`PAR`, `ALT`) are covered in
+<<async-channels-csp,Coming from JCSP or GroovyCSP>>.
+
+[[gpars-migration-not-included]]
+=== Not carried over
+
+Some GPars features have no `groovy.concurrent` counterpart:
+
+* Software transactional memory (`GParsStm`, built on Multiverse)
+* Remote actors and remote dataflow
+* `KanbanFlow`
+* `LazyDataflowVariable`
+* Dataflow operator lifecycle features such as event listeners,
+ `StopGently` and pause/resume
+
+Keep GPars for code that depends on them, or use a dedicated library for
+that concern.
diff --git a/src/spec/doc/_design-pattern-decorator.adoc b/src/spec/doc/_design-pattern-decorator.adoc
index c43c509a29..e19b0030d7 100644
--- a/src/spec/doc/_design-pattern-decorator.adoc
+++ b/src/spec/doc/_design-pattern-decorator.adoc
@@ -243,7 +243,7 @@ FINEST: StopWatch 'util.Calc.add': running time (millis) = 16
You may have to adjust your `logging.properties` file for messages at log level `FINEST` to be displayed.
-== Asynchronous Decorators using GPars
+== Asynchronous Decorators using Awaitable
The following example is inspired by some of the early example code for the
http://design.cs.iastate.edu/~panini/[Panini] programming language.
diff --git a/src/spec/doc/core-async-await.adoc b/src/spec/doc/core-async-await.adoc
index c5d940552d..028110afaf 100644
--- a/src/spec/doc/core-async-await.adoc
+++ b/src/spec/doc/core-async-await.adoc
@@ -551,4 +551,6 @@ type, e.g. `AtomicInteger` for a shared counter, or thread-safe types from
All keywords (`async`, `await`, `defer`) are contextual — they can still be
used as variable or method names in existing code.
+include::_core-gpars-migration.adoc[]
+
include::../../../subprojects/groovy-macro-library/src/spec/doc/_monadic-comprehensions.adoc[leveloffset=+1]
diff --git a/src/spec/test/AsyncAwaitSpecTest.groovy b/src/spec/test/AsyncAwaitSpecTest.groovy
index ec6da4351a..40830e13c3 100644
--- a/src/spec/test/AsyncAwaitSpecTest.groovy
+++ b/src/spec/test/AsyncAwaitSpecTest.groovy
@@ -260,6 +260,37 @@ final class AsyncAwaitSpecTest {
'''
}
+ @Test
+ void testCspProcessNetwork() {
+ assertScript '''
+ import groovy.concurrent.AsyncChannel
+ import groovy.concurrent.AsyncScope
+
+ // tag::csp_network[]
+ def numbers = AsyncChannel.create() // rendezvous, like JCSP's one2one()
+ def squares = AsyncChannel.create()
+
+ def received = AsyncScope.withScope { scope -> // PAR
+ scope.async { // GNumbers, bounded
+ for (n in 1..5) await numbers.send(n)
+ numbers.close() // the poison pill
+ }
+ scope.async { // GSquares
+ for await (n in numbers) await squares.send(n * n)
+ squares.close() // pass the poison on
+ }
+ def sink = scope.async { // GPrint, collecting
+ def seen = []
+ for await (sq in squares) seen << sq
+ seen
+ }
+ await sink
+ }
+ assert received == [1, 4, 9, 16, 25]
+ // end::csp_network[]
+ '''
+ }
+
// === Defer ===
@Test