sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Define a policy about thread pool in Apache SIS: JDK common pool reserved for short computational tasks without blocking operations, sis-util pool for longer potentially blocking tasks limited to the number of available CPU, and sis-javafx pool for immediate execution regardless if CPU are already busy or not. Initial version of TileOpExecutor capable to parallelize the operations on a RenderedImage using sis-util thread pool. First draft of ImageOperations convenience methods, with only a [...]
Date Thu, 27 Feb 2020 18:02:17 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new aaf65e7  Define a policy about thread pool in Apache SIS: JDK common pool reserved for short computational tasks without blocking operations, sis-util pool for longer potentially blocking tasks limited to the number of available CPU, and sis-javafx pool for immediate execution regardless if CPU are already busy or not. Initial version of TileOpExecutor capable to parallelize the operations on a RenderedImage using sis-util thread pool. First draft of ImageOperations convenience m [...]
aaf65e7 is described below

commit aaf65e7f0b11a8f39fd1513d928a267547fd9509
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Thu Feb 27 18:59:07 2020 +0100

    Define a policy about thread pool in Apache SIS: JDK common pool reserved for short computational tasks without blocking operations, sis-util pool for longer potentially blocking tasks limited to the number of available CPU, and sis-javafx pool for immediate execution regardless if CPU are already busy or not.
    Initial version of TileOpExecutor capable to parallelize the operations on a RenderedImage using sis-util thread pool.
    First draft of ImageOperations convenience methods, with only a `statistics` method to start with.
---
 .../apache/sis/internal/gui/BackgroundThreads.java |  16 +-
 .../java/org/apache/sis/image/ImageOperations.java |  60 ++
 .../java/org/apache/sis/image/PixelIterator.java   |  21 +-
 .../org/apache/sis/image/StatisticsCalculator.java | 104 +++
 .../internal/coverage/j2d/PropertyCalculator.java  | 265 +++++++
 .../sis/internal/coverage/j2d/TileOpExecutor.java  | 825 ++++++++++++++++++++-
 .../org/apache/sis/internal/feature/Resources.java |   5 +
 .../sis/internal/feature/Resources.properties      |   1 +
 .../sis/internal/feature/Resources_fr.properties   |   1 +
 .../apache/sis/internal/system/CommonExecutor.java | 101 +++
 .../apache/sis/internal/system/package-info.java   |   2 +-
 .../main/java/org/apache/sis/util/Exceptions.java  |  19 +-
 12 files changed, 1367 insertions(+), 53 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java
index ebae74d..9ea868e 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java
@@ -28,8 +28,13 @@ import org.apache.sis.util.logging.Logging;
 
 
 /**
- * Provides the thread pool for JavaFX application. Those threads are not daemon threads
- * in order to not stop for example in the middle of a write operation.
+ * Provides the thread pool for JavaFX application. This thread pool is different than the pool used by
+ * the {@link org.apache.sis.internal.system.CommonExecutor} shared by the rest of Apache SIS library.
+ * Contrarily to {@code CommonExecutor}, this {@code BackgroundThreads} class always allocates threads
+ * to new tasks immediately (no queuing of tasks), no matter if all processors are already busy or not.
+ * The intent is to have quicker responsiveness to user actions, even at the cost of lower throughput.
+ * Another difference is that the threads used by this class are not daemon threads in order to not stop
+ * for example in the middle of a write operation.
  *
  * <p>This class extends {@link AtomicInteger} for opportunistic reason.
  * Users should not rely on this implementation details.</p>
@@ -39,7 +44,7 @@ import org.apache.sis.util.logging.Logging;
  * @since   1.1
  * @module
  */
-@SuppressWarnings("serial")
+@SuppressWarnings("serial")                         // Not intended to be serialized.
 public final class BackgroundThreads extends AtomicInteger implements ThreadFactory {
     /**
      * The executor for background tasks. This is actually an {@link ExecutorService} instance,
@@ -56,6 +61,9 @@ public final class BackgroundThreads extends AtomicInteger implements ThreadFact
     /**
      * Creates a new thread. This method is invoked by {@link #EXECUTOR}
      * when needed and does not need to be invoked explicitly.
+     *
+     * @param  r  the runnable to assign to the thread.
+     * @return a thread for the executor.
      */
     @Override
     public Thread newThread(final Runnable r) {
@@ -72,7 +80,7 @@ public final class BackgroundThreads extends AtomicInteger implements ThreadFact
     }
 
     /**
-     * Invoked at application shutdown time for stopping the executor threads after they completed their task.
+     * Invoked at application shutdown time for stopping the executor threads after they completed their tasks.
      * This method returns soon but the background threads may continue for some time if they did not finished
      * their task yet.
      *
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java
new file mode 100644
index 0000000..97956dd
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.sis.image;
+
+import java.awt.image.RenderedImage;
+import org.apache.sis.math.Statistics;
+import org.apache.sis.util.ArgumentChecks;
+
+
+/**
+ * A predefined set of operations on images as convenience methods.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final class ImageOperations {
+    /**
+     * The default set of operations in which failures to compute cause an exception to be thrown.
+     */
+    public static final ImageOperations STRICT = new ImageOperations();
+
+    /**
+     * Creates a new set of image operations.
+     */
+    private ImageOperations() {
+    }
+
+    /**
+     * Returns statistics on all bands of the given image.
+     *
+     * @param  source  the image for which to compute statistics.
+     * @return the statistics of sample values in each band.
+     */
+    public Statistics[] statistics(final RenderedImage source) {
+        ArgumentChecks.ensureNonNull("source", source);
+        final StatisticsCalculator calculator = new StatisticsCalculator(source);
+        final Object property = calculator.getProperty(StatisticsCalculator.PROPERTY_NAME);
+        if (property instanceof Statistics[]) {
+            // TODO: check error condition.
+            return (Statistics[]) property;
+        }
+        return null;
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
index 10564d7..d7faa55 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
@@ -36,6 +36,7 @@ import org.opengis.coverage.grid.SequenceType;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.measure.NumberRange;
+import org.apache.sis.internal.coverage.j2d.PropertyCalculator;
 
 import static java.lang.Math.floorDiv;
 import static org.apache.sis.internal.util.Numerics.ceilDiv;
@@ -63,7 +64,7 @@ import static org.apache.sis.internal.util.Numerics.ceilDiv;
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   1.0
  * @module
  */
@@ -285,6 +286,18 @@ public abstract class PixelIterator {
         }
 
         /**
+         * If the given image is a wrapper doing nothing else than computing a property value,
+         * unwraps it since the iterators are not interested in properties. This unwrapping is
+         * also necessary for allowing the builder to recognize the {@link BufferedImage} case.
+         */
+        private static RenderedImage unwrap(RenderedImage image) {
+            while (image instanceof PropertyCalculator) {
+                image = ((PropertyCalculator) image).source;
+            }
+            return image;
+        }
+
+        /**
          * Creates a read-only iterator for the given raster.
          *
          * @param  data  the raster which contains the sample values on which to iterate.
@@ -307,8 +320,9 @@ public abstract class PixelIterator {
          * @param  data  the image which contains the sample values on which to iterate.
          * @return a new iterator traversing pixels in the given image.
          */
-        public PixelIterator create(final RenderedImage data) {
+        public PixelIterator create(RenderedImage data) {
             ArgumentChecks.ensureNonNull("data", data);
+            data = unwrap(data);
             if (data instanceof BufferedImage) {
                 return create(((BufferedImage) data).getRaster());
             }
@@ -381,9 +395,10 @@ public abstract class PixelIterator {
          * @param  output   the image where to write the sample values. Can be the same than {@code input}.
          * @return a new writable iterator.
          */
-        public WritablePixelIterator createWritable(final RenderedImage input, final WritableRenderedImage output) {
+        public WritablePixelIterator createWritable(RenderedImage input, final WritableRenderedImage output) {
             ArgumentChecks.ensureNonNull("input",  input);
             ArgumentChecks.ensureNonNull("output", output);
+            input = unwrap(input);
             if (order == SequenceType.LINEAR) {
                 return new LinearIterator(input, output, subArea, window);
             } else if (order != null) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java b/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java
new file mode 100644
index 0000000..89a4d81
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+package org.apache.sis.image;
+
+import java.awt.image.Raster;
+import java.awt.image.RenderedImage;
+import org.apache.sis.math.Statistics;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.internal.coverage.j2d.PropertyCalculator;
+
+
+/**
+ * Computes statistics on all pixel values of an image.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class StatisticsCalculator extends PropertyCalculator<Statistics[]> {
+    /**
+     * Name of the property computed by this class.
+     */
+    static final String PROPERTY_NAME = "statistics";
+
+    /**
+     * Creates a new calculator.
+     *
+     * @param  image  the image for which to compute statistics.
+     */
+    StatisticsCalculator(final RenderedImage image) {
+        super(image);
+    }
+
+    /**
+     * Returns the name of the property which is computed by this image.
+     */
+    @Override
+    protected String getComputedPropertyName() {
+        return PROPERTY_NAME;
+    }
+
+    /**
+     * Invoked for creating the objects holding the statistics to be computed by a single thread.
+     * This method will be invoked for each worker threads.
+     */
+    @Override
+    public Statistics[] get() {
+        final Statistics[] stats = new Statistics[source.getSampleModel().getNumBands()];
+        for (int i=0; i<stats.length; i++) {
+            stats[i] = new Statistics(Vocabulary.formatInternational(Vocabulary.Keys.Band_1, i));
+        }
+        return stats;
+    }
+
+    /**
+     * Invoked after a thread finished to process all its tiles and wants to combine its statistics
+     * with the ones computed by another thread. This method does not need to be thread-safe;
+     * synchronizations will be done by the caller.
+     *
+     * @param  previous  the statistics computed by another thread (never {@code null}).
+     * @param  computed  the statistics computed by current thread (never {@code null}).
+     * @return combination of the two results, stored in {@code previous} instances.
+     */
+    @Override
+    public Statistics[] apply(final Statistics[] previous, final Statistics[] computed) {
+        for (int i=0; i<computed.length; i++) {
+            previous[i].combine(computed[i]);
+        }
+        return previous;
+    }
+
+    /**
+     * Invoked for computing statistics on all pixel values in a raster.
+     *
+     * @param  accumulator  where to store statistics.
+     * @param  tile         the tile for which to compute statistics.
+     */
+    @Override
+    public void accept(final Statistics[] accumulator, final Raster tile) {
+        final PixelIterator it = new PixelIterator.Builder().create(tile);
+        double[] samples = null;
+        while (it.next()) {
+            samples = it.getPixel(samples);         // Get values in all bands.
+            for (int i=0; i<samples.length; i++) {
+                accumulator[i].accept(samples[i]);
+            }
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PropertyCalculator.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PropertyCalculator.java
new file mode 100644
index 0000000..114ed01
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PropertyCalculator.java
@@ -0,0 +1,265 @@
+/*
+ * 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.
+ */
+package org.apache.sis.internal.coverage.j2d;
+
+import java.util.Locale;
+import java.util.Vector;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.function.Supplier;
+import java.util.function.Consumer;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.stream.Collector;
+import java.awt.Image;
+import java.awt.Rectangle;
+import java.awt.image.ColorModel;
+import java.awt.image.SampleModel;
+import java.awt.image.RenderedImage;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+import java.awt.image.ImagingOpException;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.internal.system.Modules;
+
+
+/**
+ * An image which wraps an existing image unchanged, except for a property which is computed
+ * on the fly when first requested. All methods delegate to the wrapped image except the one
+ * for getting the property value and {@link #getSources()}.
+ *
+ * <p>The name of the computed property is given by {@link #getComputedPropertyName()}.
+ * In addition this method automatically creates another property with the same name
+ * and {@value #ERRORS_SUFFIX} suffix. That property will contain a {@link LogRecord}
+ * with the exception that occurred during tile computations, if any.
+ * The computation results are cached by this class.</p>
+ *
+ * <div class="note"><b>Design note:</b>
+ * most non-abstract methods are final because {@link org.apache.sis.image.PixelIterator}
+ * (among others) relies on the fact that it can unwrap this image and still get the same
+ * pixel values.</div>
+ *
+ * This class implements various {@link java.util.function} interfaces for implementation convenience
+ * (would not be recommended for public API, but this is an internal class). Users should not rely on
+ * this fact. Compared to lambda functions, this is one less level of indirection and makes stack traces
+ * a little bit shorter to analyze in case of exceptions.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ *
+ * @param  <A>  type of the thread-local object (the accumulator) for holding intermediate results during computation.
+ *              This is usually the final type of the property value, but not necessarily.
+ *
+ * @since 1.1
+ * @module
+ */
+public abstract class PropertyCalculator<A> implements RenderedImage,
+        Supplier<A>, BinaryOperator<A>, BiConsumer<A, Raster>, Consumer<LogRecord>
+{
+    /**
+     * The suffix to add to property name for errors that occurred during computation.
+     */
+    public static final String ERRORS_SUFFIX = ".errors";
+
+    /**
+     * The source image from which to compute the property.
+     */
+    public final RenderedImage source;
+
+    /**
+     * The computation result, or {@link Image#UndefinedProperty} if not yet computed.
+     * Note that {@code null} is a valid result.
+     */
+    private Object result;
+
+    /**
+     * The errors that occurred while computing the result, or {@code null} if none
+     * or not yet determined.
+     */
+    private LogRecord errors;
+
+    /**
+     * Creates a new calculator wrapping the given image.
+     *
+     * @param  source  the image to wrap.
+     */
+    protected PropertyCalculator(final RenderedImage source) {
+        this.source = source;
+        result = Image.UndefinedProperty;
+    }
+
+    /**
+     * Returns the source of this image. The default implementation
+     * returns {@link #source} in an vector of length 1.
+     *
+     * @return the source (usually only {@linkplain #source}) of this image.
+     */
+    @Override
+    @SuppressWarnings("UseOfObsoleteCollectionType")
+    public final Vector<RenderedImage> getSources() {
+        final Vector<RenderedImage> sources = new Vector<>(1);
+        sources.add(source);
+        return sources;
+    }
+
+    /**
+     * If the property should be computed on a subset of the tiles,
+     * the pixel coordinates of the region intersecting those tiles.
+     * The default implementation returns {@code null}.
+     *
+     * @return pixel coordinates of the region of interest, or {@code null} for the whole image.
+     */
+    protected Rectangle getAreaOfInterest() {
+        return null;
+    }
+
+    /**
+     * Returns the name of the property which is computed by this image.
+     *
+     * @return name of property computed by this image. Shall not be null.
+     */
+    protected abstract String getComputedPropertyName();
+
+    /**
+     * Returns an array of names recognized by {@link #getProperty(String)}.
+     * The default implementation returns the {@linkplain #source} properties names
+     * followed by {@link #getComputedPropertyName()} and the error property name.
+     *
+     * @return all recognized property names.
+     */
+    @Override
+    public final String[] getPropertyNames() {
+        final String name = getComputedPropertyName();
+        return ArraysExt.concatenate(source.getPropertyNames(), new String[] {name, name + ERRORS_SUFFIX});
+    }
+
+    /**
+     * Returns whether the given name is the name of the error property.
+     * The implementation of this method avoids the creation of concatenated string.
+     *
+     * @param  cn    name of the computed property.
+     * @param  name  the property name to test.
+     * @return whether {@code name} is {@code cn} + {@value #ERRORS_SUFFIX}.
+     */
+    private static boolean isErrorProperty(final String cn, final String name) {
+        return name.length() == cn.length() + ERRORS_SUFFIX.length() &&
+                    name.startsWith(cn) && name.endsWith(ERRORS_SUFFIX);
+    }
+
+    /**
+     * Gets a property from this image or from its source. If the given name is for the property
+     * to be computed by this class and if that property has not been computed before, then this
+     * method starts computation now and caches the result.
+     *
+     * @param  name  name of the property to get.
+     * @return the property for the given name (may be {@code null}).
+     */
+    @Override
+    public final Object getProperty(final String name) {
+        if (name != null) {
+            final String cn = getComputedPropertyName();
+            final boolean isProperty = cn.equals(name);
+            if (isProperty || isErrorProperty(cn, name)) {
+                synchronized (this) {
+                    if (result == Image.UndefinedProperty) {
+                        final TileOpExecutor executor = new TileOpExecutor(source, getAreaOfInterest());
+                        result = executor.executeOnReadable(source, Collector.of(this, this, this), this);
+                    }
+                    return isProperty ? result : errors;
+                }
+            }
+        }
+        return source.getProperty(name);
+    }
+
+    /**
+     * Invoked by {@link TileOpExecutor} if an error occurred while processing tiles.
+     * This method should be invoked at most once.
+     *
+     * @param  record  a description of the error that occurred.
+     */
+    @Override
+    public final synchronized void accept(final LogRecord record) {
+        if (errors != null) {
+            throw new IllegalStateException();      // Should never happen.
+        }
+        /*
+         * Completes the record with source identification as if the
+         * error occurred from above `getProperty(String)` method.
+         */
+        record.setSourceClassName(RenderedImage.class.getCanonicalName());
+        record.setSourceMethodName("getProperty");
+        record.setLoggerName(Modules.RASTER);
+        errors = record;
+    }
+
+    /**
+     * Invoked for creating the object holding the information to be computed by a single thread.
+     * This method will be invoked for each worker thread before the worker starts its execution.
+     *
+     * @return a thread-local variable holding information computed by a single thread.
+     *         May be {@code null} is such objects are not needed.
+     */
+    @Override
+    public abstract A get();
+
+    /**
+     * Invoked after a thread finished to process all its tiles and wants to combine its result with the
+     * result of another thread. This method is invoked only if {@link #get()} returned a non-null value.
+     * This method does not need to be thread-safe; synchronizations will be done by the caller.
+     *
+     * @param  previous  the result of another thread (never {@code null}).
+     * @param  computed  the result computed by current thread (never {@code null}).
+     * @return combination of the two results. May be one of the {@code previous} or {@code computed} instances.
+     */
+    @Override
+    public abstract A apply(A previous, A computed);
+
+    /**
+     * Executes this operation on the given tile. This method may be invoked from any thread.
+     * If an exception occurs during computation, that exception will be logged or wrapped in
+     * an {@link ImagingOpException} by the caller {@link TileOpExecutor}.
+     *
+     * @param  accumulator  the thread-local variable created by {@link #get()}. May be {@code null}.
+     * @param  tile         the tile on which to perform a computation.
+     * @throws RuntimeException if the calculation failed.
+     */
+    @Override
+    public abstract void accept(A accumulator, Raster tile);
+
+    /** Delegates to the wrapped image. */
+    @Override public final ColorModel     getColorModel()            {return source.getColorModel();}
+    @Override public final SampleModel    getSampleModel()           {return source.getSampleModel();}
+    @Override public final int            getWidth()                 {return source.getWidth();}
+    @Override public final int            getHeight()                {return source.getHeight();}
+    @Override public final int            getMinX()                  {return source.getMinX();}
+    @Override public final int            getMinY()                  {return source.getMinY();}
+    @Override public final int            getNumXTiles()             {return source.getNumXTiles();}
+    @Override public final int            getNumYTiles()             {return source.getNumYTiles();}
+    @Override public final int            getMinTileX()              {return source.getMinTileX();}
+    @Override public final int            getMinTileY()              {return source.getMinTileY();}
+    @Override public final int            getTileWidth()             {return source.getTileWidth();}
+    @Override public final int            getTileHeight()            {return source.getTileHeight();}
+    @Override public final int            getTileGridXOffset()       {return source.getTileGridXOffset();}
+    @Override public final int            getTileGridYOffset()       {return source.getTileGridYOffset();}
+    @Override public final Raster         getTile(int tx, int ty)    {return source.getTile(tx, ty);}
+    @Override public final Raster         getData()                  {return source.getData();}
+    @Override public final Raster         getData(Rectangle region)  {return source.getData(region);}
+    @Override public final WritableRaster copyData(WritableRaster r) {return source.copyData(r);}
+    @Override public final String         toString()                 {return source.toString();}
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java
index 0885a87..f94e9f4 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java
@@ -16,30 +16,63 @@
  */
 package org.apache.sis.internal.coverage.j2d;
 
+import java.util.Locale;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.SimpleFormatter;
+import java.util.stream.Collector;
+import java.util.function.Consumer;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
 import java.awt.Rectangle;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
 import java.awt.image.WritableRenderedImage;
 import java.awt.image.ImagingOpException;
+import org.apache.sis.util.Exceptions;
+import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.internal.feature.Resources;
+import org.apache.sis.internal.system.CommonExecutor;
 
 
 /**
- * An action to execute on each tile of an image.
- * Subclasses should override one of the following methods:
+ * A read or write action to execute on each tile of an image. The operation may be executed
+ * in a single thread or can be multi-threaded (each tile processed fully in a single thread).
+ * If the operation is to be executed in a single thread, or if the subclass is concurrent
+ * (it usually means that it does not hold any mutable state), then subclasses can override
+ * and invoke the methods in one of the following rows:
  *
- * <ul>
- *   <li>{@link #readFrom(Raster)}</li>
- *   <li>{@link #writeTo(WritableRaster)}</li>
- * </ul>
+ * <table>
+ *   <caption>Methods to use in single-thread or with concurrent implementations</caption>
+ *   <tr>
+ *     <th>Override</th>
+ *     <th>Then invoke (single thread)</th>
+ *     <th>Or invoke (multi-thread)</th>
+ *   </tr><tr>
+ *     <td>{@link #readFrom(Raster)}</td>
+ *     <td>{@link #readFrom(RenderedImage)}</td>
+ *     <td>{@link #parallelReadFrom(RenderedImage)}</td>
+ *   </tr><tr>
+ *     <td>{@link #writeTo(WritableRaster)}</td>
+ *     <td>{@link #writeTo(WritableRenderedImage)}</td>
+ *     <td>{@link #parallelWriteTo(WritableRenderedImage)}</td>
+ *   </tr>
+ * </table>
+ *
+ * <p>If the operation should be multi-threaded and produce a result, then invoke
+ * {@link #executeOnReadable executeOnReadable(…)} or {@link #executeOnWritable executeOnWritable(…)}
+ * method. Those methods are inspired from {@link java.util.stream.Stream#collect(Collector)} API.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
  * @since   1.1
  * @module
  */
-public abstract class TileOpExecutor {
+public class TileOpExecutor {
     /**
      * Minimum/maximum index of tiles to process, inclusive.
      */
@@ -47,20 +80,40 @@ public abstract class TileOpExecutor {
 
     /**
      * Creates a new operation for tiles in the specified region of the specified image.
-     * It is caller responsibility to ensure that {@code aoi} is contained in {@code image} bounds.
+     * It is caller responsibility to ensure that {@code aoi} is contained inside {@code image} bounds.
      *
      * @param  image  the image from which tiles will be fetched.
-     * @param  aoi    region of interest.
+     * @param  aoi    region of interest, or {@code null} for the whole image.
+     * @throws ArithmeticException if some tile indices are too large.
      */
     protected TileOpExecutor(final RenderedImage image, final Rectangle aoi) {
-        final int  tileWidth       = image.getTileWidth();
-        final int  tileHeight      = image.getTileHeight();
-        final long tileGridXOffset = image.getTileGridXOffset();   // We want 64 bits arithmetic in operations below.
-        final long tileGridYOffset = image.getTileGridYOffset();
-        minTileX = Math.toIntExact(Math.floorDiv(aoi.x                     - tileGridXOffset, tileWidth));
-        minTileY = Math.toIntExact(Math.floorDiv(aoi.y                     - tileGridYOffset, tileHeight));
-        maxTileX = Math.toIntExact(Math.floorDiv(aoi.x + (aoi.width  - 1L) - tileGridXOffset, tileWidth));
-        maxTileY = Math.toIntExact(Math.floorDiv(aoi.y + (aoi.height - 1L) - tileGridYOffset, tileHeight));
+        if (aoi != null) {
+            final int  tileWidth       = image.getTileWidth();
+            final int  tileHeight      = image.getTileHeight();
+            final long tileGridXOffset = image.getTileGridXOffset();   // We want 64 bits arithmetic in operations below.
+            final long tileGridYOffset = image.getTileGridYOffset();
+            minTileX = Math.toIntExact(Math.floorDiv(aoi.x                     - tileGridXOffset, tileWidth ));
+            minTileY = Math.toIntExact(Math.floorDiv(aoi.y                     - tileGridYOffset, tileHeight));
+            maxTileX = Math.toIntExact(Math.floorDiv(aoi.x + (aoi.width  - 1L) - tileGridXOffset, tileWidth ));
+            maxTileY = Math.toIntExact(Math.floorDiv(aoi.y + (aoi.height - 1L) - tileGridYOffset, tileHeight));
+        } else {
+            minTileX = image.getMinTileX();
+            minTileY = image.getMinTileY();
+            maxTileX = Math.addExact(minTileX, image.getNumXTiles() - 1);
+            maxTileY = Math.addExact(minTileY, image.getNumYTiles() - 1);
+        }
+    }
+
+    /**
+     * Returns {@code true} if this executor will apply to two tiles or more.
+     * Returns {@code false} if it will apply to a single tile or no tile at all.
+     */
+    private boolean isMultiTiled() {
+        /*
+         * Following expression is negative if at least one (max - min) value is negative
+         * (empty case), and 0 if all (max - min) values are zero (singleton case).
+         */
+        return ((maxTileX - minTileX) | (maxTileY - minTileY)) > 0;
     }
 
     /**
@@ -75,15 +128,24 @@ public abstract class TileOpExecutor {
     /**
      * Executes the read operation on the given tile.
      * The default implementation does nothing.
+     * This method should be overridden if the user intends to call {@link #readFrom(RenderedImage)} for execution
+     * in a single thread, or {@link #parallelReadFrom(RenderedImage)} for multi-threaded execution. In the single
+     * thread case, it is okay for this method to modify some mutable states in the subclass. In the multi-thread
+     * case, the subclass implementation shall be immutable or concurrent.
      *
      * @param  source  the tile to read.
+     * @throws Exception if an error occurred while processing the tile.
      */
-    protected void readFrom(Raster source) {
+    protected void readFrom(Raster source) throws Exception {
     }
 
     /**
      * Executes the write operation on the given tile.
      * The default implementation does nothing.
+     * This method should be overridden if the user intends to call {@link #writeTo(WritableRenderedImage)} for
+     * execution in a single thread, or {@link #parallelWriteTo(WritableRenderedImage)} for multi-threaded execution.
+     * In the single thread case, it is okay for this method to modify some mutable states in the subclass.
+     * In the multi-thread case, the subclass implementation shall be immutable or concurrent.
      *
      * @param  target  the tile where to write.
      * @throws Exception if an error occurred while computing the values to write.
@@ -92,64 +154,749 @@ public abstract class TileOpExecutor {
     }
 
     /**
-     * Executes the read action on tiles of the specified source image.
+     * Executes the read action sequentially on tiles of the specified source image.
      * The given source should be the same than the image specified at construction time.
      * Only tiles intersecting the area of interest will be processed.
+     * For each tile, the {@link #readFrom(Raster)} method will be invoked in current thread.
      *
      * <p>If a tile processing throws an exception, then this method stops immediately;
      * remaining tiles are not processed. This policy is suited to the cases where the
      * caller will not return any result in case of error.</p>
      *
-     * <p>Current implementation does not parallelize tile operations, because this method is
-     * invoked in contexts where it should apply on exactly one tile most of the times.</p>
+     * <p>This method does not parallelize tile operations, because it is invoked
+     * in contexts where it should apply on exactly one tile most of the times.</p>
      *
-     * @param  source  the image to read. Should be the image specified at construction time.
+     * @param  source  the image to read. This is usually the image specified at construction time,
+     *         but other images are okay if they share the same pixel and tile coordinate systems.
+     * @throws ImagingOpException if an exception occurred during {@link RenderedImage#getTile(int, int)}
+     *         or {@link #readFrom(Raster)} execution. This exception wraps the original exception as its
+     *         {@linkplain ImagingOpException#getCause() cause}.
      */
     public final void readFrom(final RenderedImage source) {
         for (int ty = minTileY; ty <= maxTileY; ty++) {
-            for (int tx = minTileX; tx <= maxTileX; tx++) {
+            for (int tx = minTileX; tx <= maxTileX; tx++) try {
                 readFrom(source.getTile(tx, ty));
+            } catch (Exception ex) {
+                Throwable e = trimImagingWrapper(ex);
+                if (!(e instanceof ImagingOpException)) {
+                    e = new ImagingOpException(Resources.format(Resources.Keys.CanNotProcessTile_2, tx, ty)).initCause(ex);
+                }
+                throw (ImagingOpException) e;
             }
         }
     }
 
     /**
-     * Executes the write action on tiles of the specified target image.
+     * Executes the write action sequentially on tiles of the specified target image.
      * The given target should be the same than the image specified at construction time.
      * Only tiles intersecting the area of interest will be processed.
+     * For each tile, the {@link #writeTo(WritableRaster)} method will be invoked in current thread.
      *
      * <p>If a tile processing throws an exception, then this method continues processing other tiles
-     * and will rethrow the exception only after all tiles have been processed. This policy is suited
-     * to the cases where the target image will continue to exist after this method call and we want
-     * to have a relatively consistent state.</p>
+     * and will throw the wrapper exception only after all tiles have been processed. This policy is
+     * suited to the cases where the target image will continue to exist after this method call and
+     * we want to have a relatively consistent state.</p>
      *
-     * <p>Current implementation does not parallelize tile operations, because this method is
-     * invoked in contexts where it should apply on exactly one tile most of the times.</p>
+     * <p>This method does not parallelize tile operations, because it is invoked
+     * in contexts where it should apply on exactly one tile most of the times.</p>
      *
-     * @param  target  the image where to write. Should be the image specified at construction time.
-     * @throws ImagingOpException if a {@link #writeTo(WritableRaster)} call threw an exception.
+     * @param  target  the image where to write. This is usually the image specified at construction time,
+     *         but other images are okay if they share the same pixel and tile coordinate systems.
+     * @throws ImagingOpException if an exception occurred during {@link WritableRenderedImage#getWritableTile(int, int)},
+     *         {@link #writeTo(WritableRaster)} or {@link WritableRenderedImage#releaseWritableTile(int, int)} execution.
+     *         This exception wraps the original exception as its {@linkplain ImagingOpException#getCause() cause}.
      */
     public final void writeTo(final WritableRenderedImage target) {
         ImagingOpException error = null;
         for (int ty = minTileY; ty <= maxTileY; ty++) {
-            for (int tx = minTileX; tx <= maxTileX; tx++) {
+            for (int tx = minTileX; tx <= maxTileX; tx++) try {
                 final WritableRaster tile = target.getWritableTile(tx, ty);
                 try {
                     writeTo(tile);
-                } catch (Exception e) {
-                    if (error == null) {
-                        error = new ImagingOpException(Resources.format(Resources.Keys.CanNotUpdateTile_2, tx, ty));
-                        error.initCause(e);
-                    } else {
-                        error.addSuppressed(e);
-                    }
                 } finally {
                     target.releaseWritableTile(tx, ty);
                 }
+            } catch (Exception ex) {
+                final Throwable e = trimImagingWrapper(ex);
+                if (error != null) {
+                    error.addSuppressed(e);
+                } else if (e instanceof ImagingOpException) {
+                    error = (ImagingOpException) e;
+                } else {
+                    error = new ImagingOpException(Resources.format(Resources.Keys.CanNotUpdateTile_2, tx, ty));
+                    error.initCause(e);
+                }
             }
         }
         if (error != null) {
             throw error;
         }
     }
+
+    /**
+     * Executes the read action in parallel on tiles of the specified source image.
+     * The given source should be the same than the image specified at construction time.
+     * Only tiles intersecting the area of interest will be processed.
+     * For each tile, the {@link #readFrom(Raster)} method will be invoked
+     * in an arbitrary thread (may be the current one).
+     *
+     * <h4>Errors management</h4>
+     * If a tile processing throws an exception, then the other threads will finish computing
+     * their current tile but no other tiles will be fetched; remaining tiles are not processed.
+     * This policy is suited to the cases where the caller will not return any result in case of error.
+     *
+     * <h4>Concurrency requirements</h4>
+     * Subclasses must override {@link #readFrom(Raster)} with a concurrent implementation.
+     * The {@link RenderedImage#getTile(int, int)} implementation of the given image must also
+     * support concurrency.
+     *
+     * @param  source  the image to read. This is usually the image specified at construction time,
+     *         but other images are okay if they share the same pixel and tile coordinate systems.
+     * @throws ImagingOpException if an exception occurred during {@link RenderedImage#getTile(int, int)}
+     *         or {@link #readFrom(Raster)} execution. This exception wraps the original exception as its
+     *         {@linkplain ImagingOpException#getCause() cause}.
+     */
+    public final void parallelReadFrom(final RenderedImage source) {
+        if (isMultiTiled()) {
+            executeOnReadable(source, executor((ignore,tile) -> {
+                try {
+                    readFrom(tile);
+                } catch (Exception ex) {
+                    throw Worker.rethrowOrWrap(ex);             // Will be caught again by Worker.run().
+                }
+            }), null);
+        } else {
+            readFrom(source);
+        }
+    }
+
+    /**
+     * Executes the write action in parallel on tiles of the specified target image.
+     * The given target should be the same than the image specified at construction time.
+     * Only tiles intersecting the area of interest will be processed.
+     * For each tile, the {@link #writeTo(WritableRaster)} method will be invoked
+     * in an arbitrary thread (may be the current one).
+     *
+     * <h4>Errors management</h4>
+     * If a tile processing throws an exception, then this method continues processing other tiles
+     * and will throw the wrapper exception only after all tiles have been processed. This policy is
+     * suited to the cases where the target image will continue to exist after this method call and
+     * we want to have a relatively consistent state.
+     *
+     * <h4>Concurrency requirements</h4>
+     * Subclasses must override {@link #writeTo(WritableRaster)} with a concurrent implementation.
+     * The {@link WritableRenderedImage#getWritableTile(int, int)} and
+     * {@link WritableRenderedImage#releaseWritableTile(int, int)} implementations
+     * of the given image must also support concurrency.
+     *
+     * @param  target  the image where to write. This is usually the image specified at construction time,
+     *         but other images are okay if they share the same pixel and tile coordinate systems.
+     * @throws ImagingOpException if an exception occurred during {@link WritableRenderedImage#getWritableTile(int, int)},
+     *         {@link #writeTo(WritableRaster)} or {@link WritableRenderedImage#releaseWritableTile(int, int)} execution.
+     *         This exception wraps the original exception as its {@linkplain ImagingOpException#getCause() cause}.
+     */
+    public final void parallelWriteTo(final WritableRenderedImage target) {
+        if (isMultiTiled()) {
+            executeOnWritable(target, executor((ignore,tile) -> {
+                try {
+                    writeTo(tile);
+                } catch (Exception ex) {
+                    throw Worker.rethrowOrWrap(ex);             // Will be caught again by Worker.run().
+                }
+            }), null);
+        } else {
+            writeTo(target);
+        }
+    }
+
+    /**
+     * Returns a collector to be used only as an executor: the accumulator is null and the combiner does nothing.
+     *
+     * @param  <RT>    either {@link Raster} or {@link WritableRaster}.
+     * @param  action  the action to execute on each tile.
+     * @return a collector which will merely act as an executor for the given action.
+     */
+    private static <RT extends Raster> Collector<RT,Void,Void> executor(final BiConsumer<Void,RT> action) {
+        return Collector.<RT,Void>of(() -> null, action, (old,ignore) -> old);
+    }
+
+    /**
+     * Executes a specified read action in parallel on all tiles of the specified image.
+     * The action is specified by 3 or 4 properties of the given {@code collector}:
+     *
+     * <ul class="verbose">
+     *   <li>
+     *     {@link Collector#supplier()} creates a new instance of type <var>A</var> for each thread
+     *     (those instances may be {@code null} if such objects are not needed).
+     *     That object does not need to be thread-safe since each instance will be used by only one thread.
+     *     Note however that the thread may use that object for processing any number of {@link Raster} tiles,
+     *     including zero.
+     *   </li><li>
+     *     {@link Collector#accumulator()} provides the consumer to execute on each tile. That consumer will
+     *     receive two arguments: the above-cited supplied instance of <var>A</var> (unique to each thread,
+     *     may be {@code null}), and the {@link Raster} instance to process. That consumer returns no value;
+     *     instead the supplied instance of <var>A</var> should be modified in-place if desired.
+     *   </li><li>
+     *     {@link Collector#combiner()} provides a function which, given two instances of <var>A</var>
+     *     computed by two different threads, combines them in a single instance of <var>A</var>.
+     *     This combiner will be invoked after a thread finished to process all its {@link Raster} tiles,
+     *     and only if the two objects to combine are not null. This combiner does not need to be thread-safe.
+     *   </li><li>
+     *     {@link Collector#finisher()} is invoked exactly once in current thread after the processing of all tiles
+     *     have been completed in all threads. This function converts the final value of <var>A</var> into the type
+     *     <var>R</var> to be returned. It is often an identity function.
+     *   </li>
+     * </ul>
+     *
+     * <h4>Errors management</h4>
+     * If an error occurred during the processing of a tile, then there is a choice:
+     *
+     * <ul class="verbose">
+     *   <li>
+     *     If the {@code errorHandler} is {@code null}, then all threads will finish the tiles they were
+     *     processing at the time the error occurred, but will not take any other tile (i.e. remaining tiles
+     *     will be left unprocessed). The exception that occurred is wrapped in an {@link ImagingOpException}
+     *     and thrown.
+     *   </li><li>
+     *     If the {@code errorHandler} is non-null, then the exception is wrapped in a {@link LogRecord} and
+     *     the processing continues with other tiles. If more exceptions happen, those subsequent exceptions
+     *     will be added to the first one by {@link Exception#addSuppressed(Throwable)}.
+     *     After all tiles have been processed, the error handler will be invoked with that {@link LogRecord}.
+     *     That {@code LogRecord} has the level, message and exception properties set, but not the source class name,
+     *     source method name or logger name. The error handler should set those properties itself.
+     *   </li>
+     * </ul>
+     *
+     * <h4>Concurrency requirements</h4>
+     * The {@link RenderedImage#getTile(int, int)} implementation of the given image must support concurrency.
+     *
+     * @param  <A>           the type of the thread-local object to be given to each thread.
+     * @param  <R>           the type of the final result. This is often the same as <var>A</var>.
+     * @param  source        the image to read. This is usually the image specified at construction time,
+     *                       but other images are okay if they share the same pixel and tile coordinate systems.
+     * @param  collector     the action to execute on each {@link Raster}, together with supplier and combiner
+     *                       of thread-local objects of type <var>A</var>. See above javadoc for more information.
+     * @param  errorHandler  where to report exceptions, or {@code null} for throwing them.
+     * @return the final result computed by finisher (may be {@code null}).
+     * @throws ImagingOpException if an exception occurred during {@link RenderedImage#getTile(int, int)}
+     *         or {@link #readFrom(Raster)} execution, and {@code errorHandler} is {@code null}.
+     * @throws RuntimeException if an exception occurred elsewhere (for example in the combiner or finisher).
+     */
+    public final <A,R> R executeOnReadable(final RenderedImage source,
+                                           final Collector<? super Raster, A, R> collector,
+                                           final Consumer<LogRecord> errorHandler)
+    {
+        ArgumentChecks.ensureNonNull("source", source);
+        ArgumentChecks.ensureNonNull("collector", collector);
+        return ReadWork.execute(this, source, collector, errorHandler);
+    }
+
+    /**
+     * Executes a specified write action in parallel on all tiles of the specified image.
+     * The action is specified by 3 or 4 properties of the given {@code collector}:
+     *
+     * <ul class="verbose">
+     *   <li>
+     *     {@link Collector#supplier()} creates a new instance of type <var>A</var> for each thread
+     *     (those instances may be {@code null} if such objects are not needed).
+     *     That object does not need to be thread-safe since each instance will be used by only one thread.
+     *     Note however that the thread may use that object for processing any number of {@link WritableRaster}
+     *     tiles, including zero.
+     *   </li><li>
+     *     {@link Collector#accumulator()} provides the consumer to execute on each tile. That consumer will
+     *     receive two arguments: the above-cited supplied instance of <var>A</var> (unique to each thread,
+     *     may be {@code null}), and the {@link WritableRaster} instance to process. That consumer returns
+     *     no value; instead the supplied instance of <var>A</var> should be modified in-place if desired.
+     *   </li><li>
+     *     {@link Collector#combiner()} provides a function which, given two instances of <var>A</var>
+     *     computed by two different threads, combines them in a single instance of <var>A</var>.
+     *     This combiner will be invoked after a thread finished to process all its {@link WritableRaster} tiles,
+     *     and only if the two objects to combine are not null. This combiner does not need to be thread-safe.
+     *   </li><li>
+     *     {@link Collector#finisher()} is invoked exactly once in current thread after the processing of all tiles
+     *     have been completed in all threads. This function converts the final value of <var>A</var> into the type
+     *     <var>R</var> to be returned. It is often an identity function.
+     *   </li>
+     * </ul>
+     *
+     * <h4>Errors management</h4>
+     * If an error occurred during the processing of a tile, the exception is remembered and the processing
+     * continues with other tiles. If more exceptions happen, those subsequent exceptions will be added to
+     * the first one by {@link Exception#addSuppressed(Throwable)}. After all tiles have been processed,
+     * there is a choice:
+     *
+     * <ul>
+     *   <li>If the {@code errorHandler} is {@code null}, the exception is wrapped in an {@link ImagingOpException} and thrown.</li>
+     *   <li>If the {@code errorHandler} is non-null, the exception is wrapped in a {@link LogRecord} and given to the handler.
+     *       That {@code LogRecord} has the level, message and exception properties set, but not the source class name,
+     *       source method name or logger name. The error handler should set those properties itself.</li>
+     * </ul>
+     *
+     * <h4>Concurrency requirements</h4>
+     * The {@link WritableRenderedImage#getWritableTile(int, int)} and
+     * {@link WritableRenderedImage#releaseWritableTile(int, int)} implementations
+     * of the given image must support concurrency.
+     *
+     * @param  <A>           the type of the thread-local object to be given to each thread.
+     * @param  <R>           the type of the final result. This is often the same as <var>A</var>.
+     * @param  target        the image where to write. This is usually the image specified at construction time,
+     *                       but other images are okay if they share the same pixel and tile coordinate systems.
+     * @param  collector     the action to execute on each {@link WritableRaster}, together with supplier and combiner
+     *                       of thread-local objects of type <var>A</var>. See above javadoc for more information.
+     * @param  errorHandler  where to report exceptions, or {@code null} for throwing them.
+     * @return the final result computed by finisher. This is often {@code null} because the purpose of calling
+     *         {@code executeOnWritable(…)} is more often to update existing tiles instead than to compute a value.
+     * @throws ImagingOpException if an exception occurred during {@link WritableRenderedImage#getWritableTile(int, int)},
+     *         {@link #writeTo(WritableRaster)} or {@link WritableRenderedImage#releaseWritableTile(int, int)} execution,
+     *         and {@code errorHandler} is {@code null}.
+     * @throws RuntimeException if an exception occurred elsewhere (for example in the combiner or finisher).
+     */
+    public final <A,R> R executeOnWritable(final WritableRenderedImage target, final Collector<? super WritableRaster,A,R> collector,
+            final Consumer<LogRecord> errorHandler)
+    {
+        ArgumentChecks.ensureNonNull("target", target);
+        ArgumentChecks.ensureNonNull("collector", collector);
+        return WriteWork.execute(this, target, collector, errorHandler);
+    }
+
+
+
+
+    /**
+     * Tile indices of the next tile to process in a multi-threaded computation. When a computation is splitted
+     * between many threads, all workers will share a reference to the same {@link Cursor} instance for fetching
+     * the indices of the next tile in iteration order no matter if requested by the same or different threads.
+     * We do that on the assumption that if calls to {@link RenderedImage#getTile(int, int)} causes read operations
+     * from a file, iteration order corresponds to consecutive tiles in the file and those tiles are loaded more
+     * efficiently with sequential read operations.
+     *
+     * <p>Current implementation uses row major iteration. A future version could use an image property giving the
+     * preferred iteration order (possibly as a list or an array of tile indices). When there is no indication about
+     * the preferred iteration order, a future version could possible uses Hilbert iterator for processing nearby
+     * tiles together (assuming they are more likely to have been computed at a near instant).</p>
+     *
+     * @param  <RI>  {@link RenderedImage} or {@link WritableRenderedImage}.
+     * @param  <A>   type of the thread-local object (the accumulator) for holding intermediate results.
+     */
+    @SuppressWarnings("serial")                 // Not intended to be serialized.
+    private final class Cursor<RI extends RenderedImage, A> extends AtomicInteger {
+        /**
+         * The image from which to read tiles or where to write tiles.
+         * In the later case, must be an instance of {@link WritableRenderedImage}.
+         */
+        final RI image;
+
+        /**
+         * The number of tiles in a row of the area of interest.
+         *
+         * @see #next(Worker)
+         */
+        private final int numXTiles;
+
+        /**
+         * The operation to execute after a thread finished to process all its tiles,
+         * for combining its result with the result of another thread.
+         *
+         * @see #accumulator
+         * @see #accumulate(Object)
+         */
+        private final BinaryOperator<A> combiner;
+
+        /**
+         * The cumulated result of all threads. Every time a thread finishes its work,
+         * it calls {@link #accumulate(Object)} for combining its result with previous
+         * value that may exist in this field.
+         *
+         * @see #combiner
+         * @see #accumulate(Object)
+         */
+        private A accumulator;
+
+        /**
+         * The errors that occurred while computing a tile, or {@code null} if none.
+         *
+         * @see #stopOnError
+         * @see #recordError(Worker, Throwable)
+         */
+        private LogRecord errors;
+
+        /**
+         * Whether to stop of the first error. If {@code true} the error will be reported as soon as possible.
+         * If {@code false}, processing of all tiles will be completed before the error is reported.
+         *
+         * @see #errors
+         * @see #recordError(Worker, Throwable)
+         */
+        private final boolean stopOnError;
+
+        /**
+         * Creates a new cursor initialized to the indices of the first tile.
+         *
+         * @param image        the image to read or the image where to write.
+         * @param collector    provides the combiner of thread-local objects of type <var>A</var>.
+         * @param stopOnError  whether to stop of the first error or to process all tiles before to report the error.
+         */
+        Cursor(final RI image, final Collector<?,A,?> collector, final boolean stopOnError) {
+            this.image       = image;
+            this.combiner    = collector.combiner();
+            this.numXTiles   = Math.incrementExact(Math.subtractExact(maxTileX, minTileX));
+            this.stopOnError = stopOnError;
+        }
+
+        /**
+         * Returns the suggested number of worker threads to create, excluding the current thread.
+         * This method always returns a value at least one 1 less than the number of tiles because
+         * the current thread will be itself a worker.
+         */
+        final int getNumWorkers() {
+            return Math.max((int) Math.min(CommonExecutor.PARALLELISM, numXTiles * (((long) maxTileY) - minTileY + 1) - 1), 0);
+        }
+
+        /**
+         * Sets the given worker to the indices of the next tile. This method is invoked by all worker thread
+         * before each new tile to process. We return tiles in iteration order, regardless which thread is
+         * requesting for next tile, for the reasons documented in {@link Cursor} javadoc.
+         *
+         * @param  indices  the worker where to update {@link Worker#tx} and {@link Worker#ty} indices.
+         * @return {@code true} if the tile at the updated indices should be processed, or {@code false}
+         *         if there is no more tile to process.
+         */
+        final boolean next(final Worker<RI,?,A> indices) {
+            final int index = getAndIncrement();
+            if (index >= 0) {
+                indices.tx = Math.addExact(minTileX, index % numXTiles);
+                indices.ty = Math.addExact(minTileY, index / numXTiles);
+                return indices.ty <= maxTileY;
+            }
+            return false;
+        }
+
+        /**
+         * Invoked when a thread finished to process all its tiles for combining its result with the result
+         * of previous threads. This method does nothing if the given result is null.
+         *
+         * @param  result  the result computed in current thread (may be {@code null}).
+         */
+        final void accumulate(final A result) {
+            if (result != null) {
+                synchronized (this) {
+                    accumulator = (accumulator == null) ? result : combiner.apply(accumulator, result);
+                }
+            }
+        }
+
+        /**
+         * Invoked after the current thread finished to process all its tiles. If some other threads are still
+         * computing their tiles, this method waits for those threads to complete (each thread has at most one
+         * tile to complete). After all threads completed, this method computes the final result and reports
+         * the errors if any.
+         *
+         * @param  <R>           the final type of the result. This is often the same type than <var>A</var>.
+         * @param  workers       handlers of all worker threads other than the current threads.
+         * @param  collector     provides the finisher to use for computing final result of type <var>R</var>.
+         * @param  errorHandler  where to report exceptions, or {@code null} for throwing them.
+         * @return the final result computed by finisher (may be {@code null}).
+         * @throws ImagingOpException if an exception occurred during {@link Worker#executeOnCurrentTile()}
+         *         and the {@code errorHandler} is {@code null}.
+         * @throws RuntimeException if an exception occurred elsewhere (for example in the combiner or finisher).
+         */
+        final <R> R finish(final Future<?>[] workers, final Collector<?,A,R> collector, final Consumer<LogRecord> errorHandler) {
+            for (final Future<?> task : workers) try {
+                task.get();
+            } catch (ExecutionException ex) {
+                /*
+                 * This is not an exception that occurred in Worker.executeOnCurrentTile(), RenderedImage.getTile(…)
+                 * or similar methods, otherwise it would have been handled by Worker.run(). This is an error or an
+                 * exception that occurred elsewhere, for example in the combiner, in which case we do not wrap it
+                 * in an ImagingOpException.
+                 */
+                throw Worker.rethrowOrWrap(ex.getCause());
+            } catch (InterruptedException ex) {
+                /*
+                 * If someone does not want to let us wait, do not wait for other worker threads neither.
+                 * We will report that interruption as an error.
+                 */
+                synchronized (this) {
+                    if (errors == null) {
+                        errors = new LogRecord(Level.WARNING, ex.toString());
+                        errors.setThrown(ex);
+                    } else {
+                        errors.getThrown().addSuppressed(ex);
+                    }
+                }
+                break;
+            }
+            /*
+             * Computes final result. The synchronization below should not be necessary since all threads
+             * finished their work, unless an `InterruptedException` has been caught above in which case
+             * it is possible that a few threads are still running.
+             */
+            final R result;
+            synchronized (this) {
+                result = collector.finisher().apply(accumulator);
+                if (errors != null) {
+                    if (errorHandler != null) {
+                        errorHandler.accept(errors);
+                    } else {
+                        final Throwable ex = trimImagingWrapper(errors.getThrown());
+                        final String message = new SimpleFormatter().formatMessage(errors);
+                        throw (ImagingOpException) new ImagingOpException(message).initCause(ex);
+                    }
+                }
+            }
+            return result;
+        }
+
+        /**
+         * Stores the given exception in a log record. We use a log record in order to initialize
+         * the timestamp and thread ID to the values they had at the time the first error occurred.
+         *
+         * @param  indices  the worker thread where the exception occurred. Its {@link Worker#tx}
+         *                  and {@link Worker#ty} indices should identify the problematic tile.
+         * @param  ex       the exception that occurred.
+         */
+        final void recordError(final Worker<RI,?,A> indices, final Throwable ex) {
+            if (stopOnError) {
+                set(Integer.MIN_VALUE);         // Will cause other threads to stop fetching tiles.
+            }
+            synchronized (this) {
+                if (errors == null) {
+                    errors = Resources.forLocale((Locale) null).getLogRecord(Level.WARNING,
+                             Resources.Keys.CanNotUpdateTile_2, indices.tx, indices.ty);
+                    errors.setThrown(ex);
+                } else {
+                    errors.getThrown().addSuppressed(ex);
+                }
+            }
+        }
+    }
+
+
+
+
+    /**
+     * Base class of workers which will read or write tiles. Exactly one {@code Worker} instance is
+     * created for each thread which will perform the computation. The same {@code Worker} instance
+     * can process an arbitrary amount of tiles.
+     *
+     * <p>Subclasses must override {@link #executeOnCurrentTile()}.</p>
+     *
+     * @param  <RI>  {@link RenderedImage} or {@link WritableRenderedImage}.
+     * @param  <RT>  {@link Raster} or {@link WritableRaster}.
+     * @param  <A>   type of the thread-local object (the accumulator) for holding intermediate results.
+     */
+    private abstract static class Worker<RI extends RenderedImage, RT extends Raster, A> implements Runnable {
+        /**
+         * An iterator over the indices of the next tiles to fetch. The same instance will be shared by all
+         * {@link Worker} instances created by the same call to {@link ReadWork#execute ReadWork.execute(…)}
+         * or {@link WriteWork#execute WriteWork.execute(…)}.
+         */
+        protected final Cursor<RI,A> cursor;
+
+        /**
+         * Indices of the tile to fetch. Those indices are updated by {@link Cursor#next(Worker)}.
+         */
+        protected int tx, ty;
+
+        /**
+         * The process to execute on each {@link Raster} or {@link WritableRaster}. Each invocation of
+         * that process will also receive the {@link #accumulator} value, which is an instance unique
+         * to each thread.
+         */
+        protected final BiConsumer<A, ? super RT> processor;
+
+        /**
+         * A thread-local variable which is given to each invocation of the {@link #processor}.
+         * Processor implementation can use this instance for storing or updating information.
+         * No synchronization is needed since this instance is not shared by other threads.
+         * This value may be {@code null} if no such object is needed.
+         */
+        protected final A accumulator;
+
+        /**
+         * Creates a new worker for traversing the tiles identified by the given cursor.
+         *
+         * @param cursor     iterator over the indices of the tiles to fetch.
+         * @param collector  provides the process to execute on each tile.
+         */
+        protected Worker(final Cursor<RI,A> cursor, final Collector<? super RT,A,?> collector) {
+            this.cursor      = cursor;
+            this.processor   = collector.accumulator();
+            this.accumulator = collector.supplier().get();
+        }
+
+        /**
+         * Invoked by {@link java.util.concurrent.ExecutorService#execute(Runnable)} for processing all tiles.
+         * This method delegates to {@link #executeOnCurrentTile()} as long as there is tiles to process.
+         * Exceptions are handled (wrapped in a {@link LogRecord} or propagated).
+         */
+        @Override
+        public final void run() {
+            while (cursor.next(this)) try {
+                executeOnCurrentTile();
+            } catch (Exception ex) {
+                cursor.recordError(this, trimImagingWrapper(ex));
+            }
+            cursor.accumulate(accumulator);
+        }
+
+        /**
+         * Gets the tiles at the ({@link #tx}, {@link #ty}) indices and processes it. If the process produces
+         * a result other than updating pixel values (for example if the process is computing statistics),
+         * then that result should be added to the {@link #accumulator} object.
+         *
+         * @throws RuntimeException if any error occurred during the process.
+         */
+        protected abstract void executeOnCurrentTile();
+
+        /**
+         * If the given exception can be propagated as an error or unchecked exception, throws it.
+         * Otherwise wraps it in an {@link ImagingOpException} with intentionally no error message
+         * (for allowing {@link #trimImagingWrapper(Throwable)} to recognize and unwrap it).
+         *
+         * @param  ex  the exception to propagate if possible.
+         * @return the exception to throw if the given exception can not be propagated.
+         */
+        static ImagingOpException rethrowOrWrap(final Throwable ex) {
+            Throwable cause = ex.getCause();
+            if (cause instanceof RuntimeException) {
+                throw (RuntimeException) cause;
+            }
+            if (cause instanceof Error) {
+                throw (Error) cause;
+            }
+            /*
+             * May occur after call to `TileOpExecutor.readFrom(…)` or `TileOpExecutor.writeTo(…)`,
+             * in which case that exception will be removed by `trimImagingWrapper(…)` method and
+             * replaced by another exception providing a more complete error message.
+             *
+             * Should not happen in other contexts because the other exception handlers where this
+             * `rethrowOrWrap(…)` method is invoked expect (indirectly) only unchecked exceptions.
+             */
+            return (ImagingOpException) new ImagingOpException(null).initCause(cause != null ? cause : ex);
+        }
+    }
+
+    /**
+     * If the given exception is a wrapper providing no useful information, returns its non-null cause.
+     * Otherwise returns the given exception, possibly {@linkplain Exceptions#unwrap(Exception) unwrapped}.
+     */
+    private static Throwable trimImagingWrapper(Throwable ex) {
+        while (ex.getClass() == ImagingOpException.class && ex.getMessage() == null && ex.getSuppressed().length == 0) {
+            final Throwable cause = ex.getCause();
+            if (cause == null) return ex;
+            ex = cause;
+        }
+        if (ex instanceof Exception) {
+            ex = Exceptions.unwrap((Exception) ex);
+        }
+        return ex;
+    }
+
+
+
+
+    /**
+     * Worker which will read tiles. Exactly one {@code ReadWork} instance is created for each thread
+     * which will perform the computation on {@link Raster} tiles. The same {@code ReadWork} instance
+     * can process an arbitrary amount of tiles.
+     *
+     * @param  <A>   type of the thread-local object (the accumulator) for holding intermediate results.
+     */
+    private static final class ReadWork<A> extends Worker<RenderedImage, Raster, A> {
+        /**
+         * Creates a new worker for traversing the tiles identified by the given cursor.
+         *
+         * @param cursor     iterator over the indices of the tiles to fetch.
+         * @param collector  provides the process to execute on each tile.
+         */
+        private ReadWork(final Cursor<RenderedImage,A> cursor, final Collector<? super Raster, A, ?> collector) {
+            super(cursor, collector);
+        }
+
+        /**
+         * Invoked by {@link Worker#run()} for processing the tile at current indices.
+         *
+         * @throws RuntimeException if any error occurred during the process.
+         */
+        @Override
+        protected void executeOnCurrentTile() {
+            final Raster tile = cursor.image.getTile(tx, ty);
+            processor.accept(accumulator, tile);
+        }
+
+        /**
+         * Implementation of {@link #executeOnReadable(RenderedImage, Collector, Consumer)}.
+         * See the Javadoc of that method for details.
+         */
+        static <A,R> R execute(final TileOpExecutor executor, final RenderedImage source,
+                final Collector<? super Raster, A, R> collector, final Consumer<LogRecord> errorHandler)
+        {
+            final Cursor<RenderedImage,A> cursor = executor.new Cursor<>(source, collector, errorHandler == null);
+            final Future<?>[] workers = new Future<?>[cursor.getNumWorkers()];
+            for (int i=0; i<workers.length; i++) {
+                workers[i] = CommonExecutor.INSTANCE.submit(new ReadWork<>(cursor, collector));
+            }
+            final ReadWork<A> worker = new ReadWork<>(cursor, collector);
+            worker.run();
+            return cursor.finish(workers, collector, errorHandler);
+        }
+    }
+
+
+
+
+    /**
+     * Worker which will write tiles. Exactly one {@code WriteWork} instance is created for each thread
+     * which will perform the operation on {@link WritableRaster} tiles. The same {@code WriteWork}
+     * instance can process an arbitrary amount of tiles.
+     *
+     * @param  <A>   type of the thread-local object (the accumulator) for holding intermediate results.
+     */
+    private static final class WriteWork<A> extends Worker<WritableRenderedImage, WritableRaster, A> {
+        /**
+         * Creates a new worker for traversing the tiles identified by the given cursor.
+         *
+         * @param cursor     iterator over the indices of the tiles to fetch.
+         * @param collector  provides the process to execute on each tile.
+         */
+        private WriteWork(final Cursor<WritableRenderedImage,A> cursor, final Collector<? super WritableRaster, A, ?> collector) {
+            super(cursor, collector);
+        }
+
+        /**
+         * Invoked by {@link Worker#run()} for processing the tile at current indices.
+         *
+         * @throws RuntimeException if any error occurred during the process.
+         */
+        @Override
+        protected void executeOnCurrentTile() {
+            final WritableRenderedImage image = cursor.image;
+            final int tx = super.tx;                                // Protect from changes (paranoiac safety).
+            final int ty = super.ty;
+            final WritableRaster tile = image.getWritableTile(tx, ty);
+            try {
+                processor.accept(accumulator, tile);
+            } finally {
+                image.releaseWritableTile(tx, ty);
+            }
+        }
+
+        /**
+         * Implementation of {@link #executeOnWritable(WritableRenderedImage, Collector, Consumer)}.
+         * See the Javadoc of that method for details.
+         */
+        static <A,R> R execute(final TileOpExecutor executor, final WritableRenderedImage target,
+                final Collector<? super WritableRaster,A,R> collector, final Consumer<LogRecord> errorHandler)
+        {
+            final Cursor<WritableRenderedImage,A> cursor = executor.new Cursor<>(target, collector, false);
+            final Future<?>[] workers = new Future<?>[cursor.getNumWorkers()];
+            for (int i=0; i<workers.length; i++) {
+                workers[i] = CommonExecutor.INSTANCE.submit(new WriteWork<>(cursor, collector));
+            }
+            final WriteWork<A> worker = new WriteWork<>(cursor, collector);
+            worker.run();
+            return cursor.finish(workers, collector, errorHandler);
+        }
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
index fef9e41..a83ddec 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
@@ -93,6 +93,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short CanNotMapToGridDimensions = 24;
 
         /**
+         * Can not process tile ({0}, {1}).
+         */
+        public static final short CanNotProcessTile_2 = 70;
+
+        /**
          * Can not set a value of type ‘{1}’ to characteristic “{0}”.
          */
         public static final short CanNotSetCharacteristics_2 = 4;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
index 3a66b14..4a69e74 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
@@ -22,6 +22,7 @@
 AbstractFeatureType_1             = Feature type \u2018{0}\u2019 is abstract.
 CanNotAssignCharacteristics_1     = Can not assign characteristics to the \u201c{0}\u201d property.
 CanNotComputeTile_2               = Can not compute tile ({0}, {1}).
+CanNotProcessTile_2               = Can not process tile ({0}, {1}).
 CanNotUpdateTile_2                = Can not update tile ({0}, {1}).
 CanNotCreateTwoDimensionalCRS_1   = Can not create a two-dimensional reference system from the \u201c{0}\u201d system.
 CanNotEnumerateValuesInRange_1    = Can not enumerate values in the {0} range.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
index 5a6009b..2dfc07f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
@@ -27,6 +27,7 @@
 AbstractFeatureType_1             = Le type d\u2019entit\u00e9 \u2018{0}\u2019 est abstrait.
 CanNotAssignCharacteristics_1     = Ne peut pas assigner des caract\u00e9ristiques \u00e0 la propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb.
 CanNotComputeTile_2               = Ne peut pas calculer la tuile ({0}, {1}).
+CanNotProcessTile_2               = Ne peut pas traiter la tuile ({0}, {1}).
 CanNotUpdateTile_2                = Ne peut pas mettre \u00e0 jour la tuile ({0}, {1}).
 CanNotCreateTwoDimensionalCRS_1   = Ne peut pas cr\u00e9er un syst\u00e8me de r\u00e9f\u00e9rence bidimensionnel \u00e0 partir du syst\u00e8me \u00ab\u202f{0}\u202f\u00bb.
 CanNotEnumerateValuesInRange_1    = Ne peut pas \u00e9num\u00e9rer les valeurs dans la plage {0}.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/system/CommonExecutor.java b/core/sis-utility/src/main/java/org/apache/sis/internal/system/CommonExecutor.java
new file mode 100644
index 0000000..6f81bd0
--- /dev/null
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/system/CommonExecutor.java
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+package org.apache.sis.internal.system;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+/**
+ * The executor shared by most of Apache SIS library for relatively "heavy" operations.
+ * The operations should relatively long tasks, otherwise work-stealing algorithms may
+ * provide better performances. For example it may be used when each computational unit
+ * is an image tile, in which case the thread scheduling overhead is small compared to
+ * the size of the computational task.
+ *
+ * <p>This thread pool is complementary to the {@link java.util.concurrent.ForkJoinPool}
+ * used by {@link java.util.stream.Stream} API for example. The fork-join mechanism expects
+ * computational tasks (blocking operations such as I/O are not recommended) of medium size
+ * (between 100 and 10000 basic computational steps). By contrast, the tasks submitted to
+ * this {@code CommonExecutor} may be long and involve blocking input/output operations.
+ * We use a separated thread pool for avoiding the risk that long and/or blocked tasks
+ * prevent the Java common pool from working.</p>
+ *
+ * <p>This class extends {@link AtomicInteger} for opportunistic reason.
+ * Users should not rely on this implementation details.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ *
+ * @see org.apache.sis.internal.gui.BackgroundThreads
+ * @see java.util.concurrent.ForkJoinPool#commonPool()
+ *
+ * @since 1.1
+ * @module
+ */
+@SuppressWarnings("serial")                     // Not intended to be serialized.
+public final class CommonExecutor extends AtomicInteger implements ThreadFactory {
+    /**
+     * Maximal number of threads that {@link #INSTANCE} can execute.
+     * If the number of tasks is greater than this parallelism value,
+     * extraneous tasks will be queued.
+     */
+    public static final int PARALLELISM = Math.max(Runtime.getRuntime().availableProcessors() - 1, 1);
+
+    /**
+     * The executor for background tasks. The maximum number of threads is the number of processors minus 1.
+     * The minus 1 is for increasing the chances that some CPU is still available for Java common thread pool
+     * or for JavaFX/Swing thread. In addition the caller will often do part of the work in its own thread.
+     * Threads are disposed after two minutes of inactivity.
+     */
+    public static final ExecutorService INSTANCE;
+    static {
+        final ThreadPoolExecutor executor = new ThreadPoolExecutor(PARALLELISM, PARALLELISM, 2, TimeUnit.MINUTES,
+            new LinkedBlockingQueue<>(1000000),             // Arbitrary limit against excessive queue expansion.
+            new CommonExecutor());
+        executor.allowCoreThreadTimeOut(true);
+        INSTANCE = executor;
+    }
+
+    /**
+     * For the singleton {@link #INSTANCE}.
+     */
+    private CommonExecutor() {
+    }
+
+    /**
+     * Invoked by {@link #INSTANCE} for creating a new daemon thread. The thread will have a slightly
+     * lower priority than normal threads so that short requests for CPU power (e.g. a user action in
+     * the JavaFX/Swing thread) are processed more quickly. This is done on the assumption that tasks
+     * executed by {@link #INSTANCE} are relatively long tasks, for which loosing momentously some CPU
+     * power does not make a big difference.
+     *
+     * @param  r  the runnable to assign to the thread.
+     * @return a thread for the executor.
+     */
+    @Override
+    public Thread newThread(final Runnable r) {
+        final Thread t = new Thread(Threads.SIS, r, "Background worker #" + incrementAndGet());
+        t.setPriority(Thread.NORM_PRIORITY - 2);
+        t.setDaemon(true);
+        return t;
+    }
+}
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/system/package-info.java b/core/sis-utility/src/main/java/org/apache/sis/internal/system/package-info.java
index 4d1292e..998357c 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/system/package-info.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/system/package-info.java
@@ -24,7 +24,7 @@
  * may change in incompatible ways in any future version without notice.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.3
  * @module
  */
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/Exceptions.java b/core/sis-utility/src/main/java/org/apache/sis/util/Exceptions.java
index 850544b..c6239dc 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/Exceptions.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/Exceptions.java
@@ -24,6 +24,7 @@ import java.sql.SQLException;
 import java.io.UncheckedIOException;
 import java.nio.file.DirectoryIteratorException;
 import java.lang.reflect.InvocationTargetException;
+import java.util.concurrent.ExecutionException;
 import org.opengis.util.InternationalString;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.collection.BackingStoreException;
@@ -33,7 +34,7 @@ import org.apache.sis.util.collection.BackingStoreException;
  * Static methods working with {@link Exception} instances.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.3
  * @module
  */
@@ -231,6 +232,7 @@ public final class Exceptions extends Static {
      *
      * <ul>
      *   <li>It is an instance of {@link InvocationTargetException} (could be wrapping anything).</li>
+     *   <li>It is an instance of {@link ExecutionException} (could be wrapping anything).</li>
      *   <li>It is an instance of {@link BackingStoreException} (typically wrapping a checked exception).</li>
      *   <li>It is an instance of {@link UncheckedIOException} (wrapping a {@link java.io.IOException}).</li>
      *   <li>It is an instance of {@link DirectoryIteratorException} (wrapping a {@link java.io.IOException}).</li>
@@ -242,8 +244,8 @@ public final class Exceptions extends Static {
      * {@link java.security.PrivilegedActionException} is also a wrapper exception, but is not included in above list
      * because it is used in very specific contexts.</div>
      *
-     * This method uses only the exception class as criterion;
-     * it does not verify if the exception messages are the same.
+     * This method uses only the exception class and the absence of {@linkplain Exception#getSuppressed() suppressed
+     * exceptions} as criterion; it does not verify if the exception messages are the same.
      *
      * @param  exception  the exception to unwrap (may be {@code null}.
      * @return the unwrapped exception (may be the given argument itself).
@@ -252,16 +254,21 @@ public final class Exceptions extends Static {
      */
     public static Exception unwrap(Exception exception) {
         if (exception != null) {
-            while (exception instanceof InvocationTargetException ||
+            while (exception.getSuppressed().length == 0 &&
+                  (exception instanceof InvocationTargetException ||
+                   exception instanceof ExecutionException ||
                    exception instanceof BackingStoreException ||
                    exception instanceof UncheckedIOException ||
-                   exception instanceof DirectoryIteratorException)
+                   exception instanceof DirectoryIteratorException))
             {
                 final Throwable cause = exception.getCause();
                 if (!(cause instanceof Exception)) break;
                 exception = (Exception) cause;
             }
-            for (Throwable cause; exception.getClass().isInstance(cause = exception.getCause());) {
+            Throwable cause;
+            while (exception.getSuppressed().length == 0 &&
+                   exception.getClass().isInstance(cause = exception.getCause()))
+            {
                 exception = (Exception) cause;      // Should never fail because of isInstance(…) check.
             }
         }


Mime
View raw message