[m-rev.] diff: [java] Fix non-termination when main/2 throws an exception

Paul Bone paul at bone.id.au
Tue Dec 30 17:11:35 AEDT 2014


Branches: master (the bug only exists on master).

---
[java] Fix non-termination when main/2 throws an exception

When main/2 throws an exception we did not properly shutdown the thread pool
and therefore the JVM would not shut down.  Simply calling shutdown() in a
finally block is insufficient because then the primordial thread may finish
before the worker thread is able to report the exception thrown by main/2.
This doesn't seem right because the JVM is supposed to wait for all the
non-daemon threads to finish before it exits.  I suspect that the primordial
thread is closing stdout and stderr as it exits and therefore the exception
is never seen, but I don't know.

This change fixes the issue by ensuring that shutdown() is always called (in
a finally block) and that the main thread waits for the thread pool to
shutdown before it exits.

java/runtime/MercuryThreadPool.java:
    runMain() will not exit until the worker threads have exited.

    Create a new method waitForShutdown() that will wait for the thread pool
    to shutdown.

    Signal the main thread when a worker thread exits.

java/runtime/MercuryWorkerThread.java:
    Worker threads now exit if their task raises an unhanded exception.

java/runtime/MercuryRuntime.java:
    Allow standalone programs to have the same behavour as programs whose
    entrypoint is written in Mercury.
---
 java/runtime/MercuryRuntime.java      |  7 ++-
 java/runtime/MercuryThreadPool.java   | 87 +++++++++++++++++++++++++++++------
 java/runtime/MercuryWorkerThread.java |  2 +
 3 files changed, 82 insertions(+), 14 deletions(-)

diff --git a/java/runtime/MercuryRuntime.java b/java/runtime/MercuryRuntime.java
index a50b9f6..b3b8391 100644
--- a/java/runtime/MercuryRuntime.java
+++ b/java/runtime/MercuryRuntime.java
@@ -50,10 +50,15 @@ public class MercuryRuntime
      * Finalise the runtime system.
      * This _must_ be called at the normal end of any program.  It runs
      * finalisers and stops the thread pool.
+     * This will wait for the thread pool to shutdown.
      */
     public static void finalise() {
+        MercuryThreadPool pool;
+
+        pool = getThreadPool();
         JavaInternal.run_finalisers();
-        getThreadPool().shutdown();
+        pool.shutdown();
+        pool.waitForShutdown();
     }
 }
 
diff --git a/java/runtime/MercuryThreadPool.java b/java/runtime/MercuryThreadPool.java
index 4133907..676f2ef 100644
--- a/java/runtime/MercuryThreadPool.java
+++ b/java/runtime/MercuryThreadPool.java
@@ -311,6 +311,8 @@ public class MercuryThreadPool
         } finally {
             threads_lock.unlock();
         }
+
+        signalMainLoop();
     }
 
     public void taskDone(Task task)
@@ -346,7 +348,12 @@ public class MercuryThreadPool
     {
         main_loop_lock.lock();
         try {
-            main_loop_condition.signal();
+            /*
+             * There may be more than one thread waiting on this condition
+             * such as when more than one thread calls waitForShutdown().
+             * I can't imagine this happening, bit it is allowed.
+             */
+            main_loop_condition.signalAll();
         } finally {
             main_loop_lock.unlock();
         }
@@ -407,8 +414,7 @@ public class MercuryThreadPool
              */
             threads_lock.lock();
             try {
-                int num_threads = num_threads_working + num_threads_waiting +
-                    num_threads_blocked + num_threads_other;
+                int num_threads = numThreads();
                 int num_threads_limit = thread_pool_size +
                     num_threads_blocked;
                 // Determine the number of new threads that we want.
@@ -452,6 +458,15 @@ public class MercuryThreadPool
     }
 
     /**
+     * Get the total number of threads.
+     * Caller must hold threads lock.
+     */
+    private int numThreads() {
+        return num_threads_working + num_threads_waiting +
+            num_threads_blocked + num_threads_other;
+    }
+
+    /**
      * Run the thread pool.
      * The calling thread is used to "run" the thread pool.  Its main job
      * is to keep the correct number of worker threads alive.  It does not
@@ -542,20 +557,56 @@ public class MercuryThreadPool
             }
         }
 
-        /*
-         * Shutdown
-         */
+        doShutdown();
+    }
+
+    /**
+     * Perform the shutdown.
+     */
+    private void doShutdown()
+    {
         tasks_lock.lock();
         try {
             shutdown_now = true;
-            thread_wait_for_task_condition.signalAll();
             running = false;
+            thread_wait_for_task_condition.signalAll();
         } finally {
             tasks_lock.unlock();
         }
     }
 
     /**
+     * Wait for all the worker threads to exit.
+     * Even though the JVM is not supposed to exit until all the non-daemon
+     * threads have exited the effects of some threads may get lost.  I
+     * suspect this may be because the main thread closes stdout and stderr.
+     * Waiting for the worker threads fixes the problem - pbone.
+     */
+    public boolean waitForShutdown()
+    {
+        boolean has_shutdown = false;
+
+        if (shutdown_request) {
+            do {
+                main_loop_lock.lock();
+                try {
+                    has_shutdown = numThreads() == 0;
+                    if (!has_shutdown) {
+                        main_loop_condition.await();
+                    }
+                } catch (InterruptedException e) {
+                    continue;
+                } finally {
+                    main_loop_lock.unlock();
+                }
+            } while (!has_shutdown);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
      * Start the thread pool in its own thread.
      * Normally the thread pool ie executed directly by the main thread.
      * However, when Mercury is used as a library by a native Java
@@ -589,12 +640,13 @@ public class MercuryThreadPool
 
     /**
      * Request that the thread pool shutdown.
-     * This method does not wait for the thread pool to shutdown, it's an
-     * asynchronous signal.  The thread pool will shutdown if: shutdown() has
+     * The thread pool will shutdown if: shutdown() has
      * been called (implicitly when main/2 is written in Mercury) and there
      * are no remaining tasks either queued or running (spawn_native tasks
-     * are not included).  The requirement that the process does not exit
-     * until all tasks have finish is maintained by the JVM.
+     * are not included).
+     *
+     * This method is asynchronous, it will not wait for the thread pool to
+     * shutdown.
      */
     public boolean shutdown()
     {
@@ -624,8 +676,11 @@ public class MercuryThreadPool
 
         run_main_and_shutdown = new Runnable() {
             public void run() {
-                run_main.run();
-                shutdown();
+                try {
+                    run_main.run();
+                } finally {
+                    shutdown();
+                }
             }
         };
         main_task = new Task(run_main_and_shutdown);
@@ -649,6 +704,12 @@ public class MercuryThreadPool
              */
             run();
             jmercury.runtime.JavaInternal.run_finalisers();
+            /*
+             * We always wait for the thread pool to shutdown as worker
+             * threads may either be completing work or reporting the reason
+             * why the runtime is aborting.
+             */
+            waitForShutdown();
         } catch (jmercury.runtime.Exception e) {
             JavaInternal.reportUncaughtException(e);
         }
diff --git a/java/runtime/MercuryWorkerThread.java b/java/runtime/MercuryWorkerThread.java
index a3d0d4f..994b30d 100644
--- a/java/runtime/MercuryWorkerThread.java
+++ b/java/runtime/MercuryWorkerThread.java
@@ -59,6 +59,8 @@ public class MercuryWorkerThread extends MercuryThread
                     // The task threw a Mercury exception.
                     pool.taskFailed(task, e);
                     JavaInternal.reportUncaughtException(e);
+                    // Make the thread exit after throwing an exception.
+                    break;
                 } catch (Throwable e) {
                     // Some other error occured. bail out.
                     System.err.println("Uncaught exception: " + e.toString());
-- 
2.1.3




More information about the reviews mailing list