Skip to content

Commit 652e737

Browse files
committed
Refactor ChunkyThread joinAll to give stronger guarantees
And hopefully be more readable
1 parent b2e60ed commit 652e737

1 file changed

Lines changed: 51 additions & 33 deletions

File tree

chunky/src/java/se/llbit/util/concurrent/ChunkyThread.java

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public synchronized static ForkJoinPool addForkJoinPool(ForkJoinPool pool) {
8989
*
9090
* @param timeout The maximum time to wait <b><u>AFTER</u></b> a shutdown is initiated
9191
* @param unit the time unit of the timeout argument
92+
*
9293
* @return Whether all threads were joined before returning
9394
*/
9495
public static boolean joinAll(long timeout, @NotNull TimeUnit unit) {
@@ -102,55 +103,35 @@ public static boolean joinAll(long timeout, @NotNull TimeUnit unit) {
102103

103104
while (true) {
104105
try {
105-
// must wait for the latch as hitting the for loop below first causes immediate evaluation of the
106-
// for loop iterator, potentially missing new threads.
106+
// Must wait for the latch as hitting the for loop below first causes immediate evaluation of:
107+
// - The for loop iterator, potentially missing new threads.
108+
// - The end time, meaning waiting starts before shutdown begins.
107109
shutdownLatch.await();
108110
break;
109111
} catch (InterruptedException e) {
110112
interrupted = true;
111113
}
112114
}
113115

114-
long startTime = System.nanoTime();
115-
long endTime = startTime + unit.toNanos(timeout);
116-
117-
boolean anyAlive = false;
118-
116+
long endTime = System.nanoTime() + unit.toNanos(timeout);
119117
try {
120-
for (ExecutorService executorService : executorServices) {
121-
while (System.nanoTime() < endTime) {
122-
try {
123-
long waitTime = endTime - startTime;
124-
if (waitTime > 0) {
125-
executorService.awaitTermination(waitTime, TimeUnit.NANOSECONDS);
126-
}
127-
break;
128-
} catch (InterruptedException e) {
129-
interrupted = true;
130-
}
131-
}
132-
anyAlive |= !executorService.isTerminated();
133-
}
134-
for (Thread thread : ChunkyThread.threads) {
135-
while (System.nanoTime() < endTime) {
136-
try {
137-
long waitTimeMillis = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);
138-
if (waitTimeMillis > 0) {
139-
thread.join(waitTimeMillis);
140-
}
141-
break;
142-
} catch (InterruptedException e) {
143-
interrupted = true;
118+
while (System.nanoTime() < endTime) {
119+
try {
120+
if (joinAllInterruptable(endTime)) {
121+
// All threads are joined, skip the rest of the wait time.
122+
return true;
144123
}
124+
} catch (InterruptedException e) {
125+
interrupted = true;
145126
}
146-
anyAlive |= thread.isAlive();
147127
}
128+
// Got to the end of the wait time without joining everything, can give no guarantees
129+
return false;
148130
} finally {
149131
if (interrupted) {
150132
Thread.currentThread().interrupt();
151133
}
152134
}
153-
return !anyAlive;
154135
}
155136

156137
/**
@@ -178,6 +159,43 @@ public static boolean interruptAndJoinAll(long timeout, @NotNull TimeUnit unit)
178159
return joinAll(timeout, unit);
179160
}
180161

162+
/**
163+
* Await the joining of all threads managed by chunky.
164+
*
165+
* <p>This method is only safe to call if the {@link ChunkyThread#shutdownLatch} has been set.</p>
166+
*
167+
* <p><b><i>WARNING: calling this from any thread registered with {@link #addThread(Thread)} may <u>deadlock</u>.</i></b></p>
168+
*
169+
* @param endTimeNanos The time at which to stop waiting.
170+
*
171+
* @return Whether all threads were joined before returning
172+
*
173+
* @throws InterruptedException Propagates up when interrupted. The caller has no guarantee that shutdown has begun,
174+
* or that any of the inner threads have been joined.
175+
*/
176+
private static boolean joinAllInterruptable(long endTimeNanos) throws InterruptedException {
177+
// The intention here whether we return true or false, is to give the caller the most complete acquire load possible.
178+
// Even if we reach the timeout given by the caller, we still establish a happens-before with every dead thread.
179+
180+
boolean anyAlive = false;
181+
for (ExecutorService executorService : executorServices) {
182+
long waitTime = endTimeNanos - System.nanoTime();
183+
anyAlive |= !executorService.awaitTermination(waitTime, TimeUnit.NANOSECONDS);
184+
}
185+
for (Thread thread : threads) {
186+
long waitTime = endTimeNanos - System.nanoTime();
187+
if (waitTime > 0) {
188+
thread.join(waitTime); // joining with 0 is infinite wait time, very intuitive.
189+
}
190+
// Thread.isAlive() establishes a happens-before with the thread. As such the following are non-issues:
191+
// - Not joining the thread, if waitTime <= 0
192+
// - The thread stopping between Thread.join() and Thread.isAlive().
193+
anyAlive |= thread.isAlive();
194+
}
195+
196+
return !anyAlive;
197+
}
198+
181199
private void setDefaults() {
182200
this.setDaemon(true);
183201
addThread(this);

0 commit comments

Comments
 (0)