Skip to content

Commit

Permalink
WIP - migrating missing thread tools methods
Browse files Browse the repository at this point in the history
  • Loading branch information
TomaszTB committed Oct 30, 2024
1 parent 0eae83b commit 7fde7dc
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 5 deletions.
87 changes: 82 additions & 5 deletions src/main/java/us/ihmc/commons/thread/ThreadTools.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package us.ihmc.commons.thread;

import us.ihmc.commons.Conversions;
import us.ihmc.commons.RunnableThatThrows;
import us.ihmc.commons.exception.DefaultExceptionHandler;
import us.ihmc.commons.exception.ExceptionHandler;
import us.ihmc.commons.exception.ExceptionTools;
Expand All @@ -12,6 +13,7 @@
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;

/**
* <p>
Expand Down Expand Up @@ -125,6 +127,52 @@ public static void sleepForever()
}
}

/**
* Very similar to {@link #sleepSeconds(double)}, but uses {@link LockSupport#parkNanos} to sleep.
* {@link LockSupport#parkNanos} is more accurate than {@link Thread#sleep}.
* The requested sleep is guaranteed to be at least as long as the requested
* amount and can be up to a nanosecond longer.
*/
public static void park(double seconds)
{
double floatingNanos = seconds * 1e9;
long nanoseconds = (long) floatingNanos;

if (floatingNanos > nanoseconds) // Take nanosecond ceiling instead of floor
++nanoseconds;

LockSupport.parkNanos(nanoseconds); // More accurate than Thread.sleep
}

/**
* Guarantees a sleep of a minimum duration in floating point seconds
* using {@link LockSupport#parkNanos}. It will always sleep a little too long.
* The amount overslept probably varies by system, but it has been observed to
* be less than half a millisecond.
* <p>
* {@link #sleepSeconds} can return slightly early because it
* cuts off the sub-nanosecond part, allowing it to under-sleep by a nanosecond
* at most.
*
* @param duration to sleep in seconds
* @return Exactly how long it actually slept in seconds
*/
public static double parkAtLeast(double duration)
{
double startTime = Conversions.nanosecondsToSeconds(System.nanoTime());
double amountSlept = 0.0;
do
{
double nextDuration = duration - amountSlept;

park(nextDuration);

amountSlept = Conversions.nanosecondsToSeconds(System.nanoTime()) - startTime;
}
while (amountSlept < duration);
return amountSlept;
}

/**
* Join from current thread, printing stack trace if interrupted.
*/
Expand Down Expand Up @@ -164,6 +212,24 @@ public static Thread startAsDaemon(Runnable daemonThreadRunnable, String threadN
return daemonThread;
}

/**
* Starts a user thread for a {@linkplain RunnableThatThrows}.
* To start a daemon thread
*/
public static Thread startAThread(RunnableThatThrows runnable, ExceptionHandler exceptionHandler, String threadName)
{
return startAThread(() -> ExceptionTools.handle(runnable, exceptionHandler), threadName);
}

/**
* Starts a damon thread for a {@linkplain RunnableThatThrows}.
* The Java Virtual Machine exits when the only threads running are all daemon threads.
*/
public static Thread startAsDaemon(RunnableThatThrows runnable, ExceptionHandler exceptionHandler, String threadName)
{
return startAsDaemon(() -> ExceptionTools.handle(runnable, exceptionHandler), threadName);
}

public static void waitUntilNextMultipleOf(long waitMultipleMS) throws InterruptedException
{
waitUntilNextMultipleOf(waitMultipleMS, 0);
Expand Down Expand Up @@ -204,10 +270,8 @@ public static ThreadFactory getNamedThreadFactory(String name)
*/
public static ThreadFactory createNamedThreadFactory(String prefix)
{
boolean includePoolInName = true;
boolean includeThreadNumberInName = true;
boolean daemon = false;
return createNamedThreadFactory(prefix, includePoolInName, includeThreadNumberInName, daemon, Thread.NORM_PRIORITY);
return createNamedThreadFactory(prefix, daemon);
}

/**
Expand All @@ -218,11 +282,24 @@ public static ThreadFactory createNamedThreadFactory(String prefix)
* @return thread factory
*/
public static ThreadFactory createNamedDaemonThreadFactory(String prefix)
{
boolean daemon = true;
return createNamedThreadFactory(prefix, daemon);
}

/**
* Thread factory that creates threads with normal priority
* with the naming scheme "name-pool-1-thread-1", "name-pool-1-thread-2", ...
*
* @param prefix useful name to identify the purpose of threads
* @param daemon set threads to daemon
* @return thread factory
*/
public static ThreadFactory createNamedThreadFactory(String prefix, boolean daemon)
{
boolean includePoolInName = true;
boolean includeThreadNumberInName = true;
boolean daemon = true;
return createNamedThreadFactory(prefix, includePoolInName, includeThreadNumberInName, daemon, Thread.NORM_PRIORITY);
return ThreadTools.createNamedThreadFactory(prefix, includePoolInName, includeThreadNumberInName, daemon, Thread.NORM_PRIORITY);
}

/**
Expand Down
83 changes: 83 additions & 0 deletions src/test/java/us/ihmc/commons/thread/ThreadToolsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import org.junit.jupiter.api.Test;
import us.ihmc.commons.Conversions;
import us.ihmc.commons.exception.DefaultExceptionHandler;
import us.ihmc.commons.exception.ExceptionTools;
import us.ihmc.commons.time.Stopwatch;
import us.ihmc.log.LogTools;

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
Expand Down Expand Up @@ -278,6 +281,86 @@ public void testThreadSleepEvenWhenInterrupted()
}
}

@Test
public void testParkAtLeast()
{
assertTrue(conductParkTest(0.0000000000001, false));
assertTrue(conductParkTest(0.5e-9, false));
assertTrue(conductParkTest(1e-9, false));
assertTrue(conductParkTest(0.1, false));
assertTrue(conductParkTest(0.0001, false));
assertTrue(conductParkTest(0.0000000005, false));
assertTrue(conductParkTest(1.1, false));
assertTrue(conductParkTest(2.0, false));

assertTrue(conductParkTest(0.0000000000001, true));
assertTrue(conductParkTest(0.5e-9, true));
assertTrue(conductParkTest(1e-9, true));
assertTrue(conductParkTest(0.1, true));
assertTrue(conductParkTest(0.0001, true));
assertTrue(conductParkTest(0.0000000005, true));
assertTrue(conductParkTest(1.1, true));
assertTrue(conductParkTest(2.0, true));
}

private boolean conductParkTest(double sleepDuration, boolean atLeast)
{
double before = Conversions.nanosecondsToSeconds(System.nanoTime());

if (atLeast)
ThreadTools.parkAtLeast(sleepDuration);
else
ThreadTools.park(sleepDuration);

double after = Conversions.nanosecondsToSeconds(System.nanoTime());

double overslept = (after - before) - sleepDuration;

// FIXME?
// LogTools.info("Overslept %f ms".formatted(Conversions.secondsToMilliseconds(overslept)));

assertTrue(overslept < 0.005); // Assert we don't oversleep more than 5 milliseconds -- typically a lot lower

return overslept > 0.0;
}

@Test
public void testCancellableScheduledTasks()
{
ScheduledExecutorService scheduler = ThreadTools.newSingleDaemonThreadScheduledExecutor("Test");

StringBuilder output = new StringBuilder();

ScheduledFuture<?> scheduledFuture1 = scheduler.schedule(() -> output.append("A"), 400, TimeUnit.MILLISECONDS);
ThreadTools.sleep(200);
scheduledFuture1.cancel(false);
scheduler.schedule(() -> output.append("B"), 400, TimeUnit.MILLISECONDS);
ThreadTools.sleep(600);
ScheduledFuture<StringBuilder> scheduledFuture2 = scheduler.schedule(() -> output.append("C"), 400, TimeUnit.MILLISECONDS);
ThreadTools.sleep(200);
scheduledFuture2.cancel(false);
ThreadTools.sleep(600);
scheduler.schedule(() -> output.append("D"), 400, TimeUnit.MILLISECONDS);
ThreadTools.sleep(600);

scheduler.schedule(() -> ExceptionTools.handle(() ->
{
output.append("E");
throw new NullPointerException();
}, DefaultExceptionHandler.PRINT_MESSAGE), 400, TimeUnit.MILLISECONDS);
ThreadTools.sleep(600);
ScheduledFuture<StringBuilder> scheduledFuture3 = scheduler.schedule(() -> output.append("F"), 400, TimeUnit.MILLISECONDS);
ThreadTools.sleep(200);
scheduledFuture3.cancel(false);
ThreadTools.sleep(600);

String recordedOutput = output.toString();
assertEquals("BDE", recordedOutput);
LogTools.info(recordedOutput);

scheduler.shutdown();
}

private class SleepAndVerifyDespiteWakingUpRunnable implements Runnable
{
private long millisecondsToSleep;
Expand Down

0 comments on commit 7fde7dc

Please sign in to comment.