Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to achieve a guaranteed sleep time on a thread

I have a requirement for a class method to be called every 50 milliseconds. I don't use Thread.sleep because it's very important that it happens as precisely as possible to the milli, whereas sleep only guarantees a minimum time. The basic set up is this:

public class ClassA{
    public void setup(){
        ScheduledExecutorService se = Executors.newScheduledThreadPool(20);
        se.scheduleAtFixedRate(this::onCall, 2000, 50, TimeUnit.MILLISECONDS);
    }

    protected void onCall(Event event) {
        // do something
    }
}

Now this by and large works fine. I have put System.out.println(System.nanoTime) in onCall to check its being called as precisely as I hope it is. I have found that there is a drift of 1-5 milliseconds over the course of 100s of calls, which corrects itself now and again.

A 5 ms drift unfortunately is pretty hefty for me. 1 milli drift is ok but at 5ms it messes up the calculation I'm doing in onCall because of states of other objects. It would be almost OK if I could get the scheduler to auto-correct such that if it's 5ms late on one call, the next one would happen in 45ms instead of 50.

My question is: Is there a more precise way to achieve this in Java? The only solution I can think of at the moment is to call a check method every 1ms and check the time to see if its at the 50ms mark. But then I'd need to maintain some logic if, on the off-chance, the precise 50ms interval is missed (49,51).

Thanks

like image 369
Manish Patel Avatar asked Jan 05 '23 18:01

Manish Patel


2 Answers

Can I achieve a guaranteed sleep time on a thread?

Sorry, but No.

There is no way to get reliable, precise delay timing in a Java SE JVM. You need to use a Real time Java implementation running on a real time operating system.


Here are a couple of reasons why Java SE on a normal OS cannot do this.

  1. At certain points, the GC in a Java SE JVM needs to "stop the world". While this is happening, no user thread can run. If your timer goes off in a "stop the world" pause, it can't be scheduled until the pause is over.

  2. Scheduling of threads in a JVM is actually done by the host operating system. If the system is busy, the host OS may decide not to schedule the JVM's threads when your application needs this to happen.


The java.util.Timer.scheduleAtFixedRate approach is probably as good as you will get on Java SE. It should address long-term drift, but you can't get rid of the "jitter". And that jitter could easily be hundreds of milliseconds ... or even seconds.

Spinlocks won't help if the system is busy and the OS is preempting or not scheduling your threads. (And spinlocking in user code is wasteful ...)

like image 108
Stephen C Avatar answered Jan 21 '23 21:01

Stephen C


According to the comment, the primary goal is not to concurrently execute multiple tasks at this precise interval. Instead, the goal is to execute a single task at this interval as precisely as possible.

Unfortunately, neither the ScheduledExecutorService nor any manual constructs involving Thread#sleep or LockSupport#parkNanos are very precise in that sense. And as pointed out in the other answers: There may always be influencing factors that are beyond your control - namely, details of the JVM implementation, garbage collection, JIT runs etc.

Nevertheless, a comparatively simple approach to achieve a high precision here is busy waiting. (This was already mentioned in an answer that is now deleted). But of course, this has several caveats. Most importantly, it will burn processing resources of one CPU. (And on a single-CPU-system, this may be particularly bad).

But in order to show that it may be far more precise than other waiting approaches, here is a simple comparison of the ScheduledExecutorService approach and the busy waiting:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class PreciseSchedulingTest
{
    public static void main(String[] args)
    {
        long periodMs = 50;
        PreciseSchedulingA a = new PreciseSchedulingA();
        a.setup(periodMs);

        PreciseSchedulingB b = new PreciseSchedulingB();
        b.setup(periodMs);
    }
}

class CallTracker implements Runnable
{
    String name;
    long expectedPeriodMs;
    long baseTimeNs;
    long callTimesNs[];
    int numCalls;
    int currentCall;

    CallTracker(String name, long expectedPeriodMs)
    {
        this.name = name;
        this.expectedPeriodMs = expectedPeriodMs;
        this.baseTimeNs = System.nanoTime();
        this.numCalls = 50;
        this.callTimesNs = new long[numCalls];
    }

    @Override
    public void run()
    {
        callTimesNs[currentCall] = System.nanoTime();
        currentCall++;
        if (currentCall == numCalls)
        {
            currentCall = 0;
            double maxErrorMs = 0;
            for (int i = 1; i < numCalls; i++)
            {
                long ns = callTimesNs[i] - callTimesNs[i - 1];
                double ms = ns * 1e-6;
                double errorMs = ms - expectedPeriodMs;
                if (Math.abs(errorMs) > Math.abs(maxErrorMs))
                {
                    maxErrorMs = errorMs;
                }
                //System.out.println(errorMs);
            }
            System.out.println(name + ", maxErrorMs : " + maxErrorMs);
        }
    }

}

class PreciseSchedulingA
{
    public void setup(long periodMs)
    {
        CallTracker callTracker = new CallTracker("A", periodMs);
        ScheduledExecutorService se = Executors.newScheduledThreadPool(20);
        se.scheduleAtFixedRate(callTracker, periodMs, 
            periodMs, TimeUnit.MILLISECONDS);
    }
}

class PreciseSchedulingB
{
    public void setup(long periodMs)
    {
        CallTracker callTracker = new CallTracker("B", periodMs);

        Thread thread = new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                while (true)
                {
                    long periodNs = periodMs * 1000 * 1000;
                    long endNs = System.nanoTime() + periodNs;
                    while (System.nanoTime() < endNs) 
                    {
                        // Busy waiting...
                    }
                    callTracker.run();
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
    }
}

Again, this should be taken with a grain of salt, but the results on My Machine® are as follows:

A, maxErrorMs : 1.7585339999999974
B, maxErrorMs : 0.06753599999999693
A, maxErrorMs : 1.7669149999999973
B, maxErrorMs : 0.007193999999998368
A, maxErrorMs : 1.7775299999999987
B, maxErrorMs : 0.012780999999996823

showing that the error for the waiting times is in the range of few microseconds.

In order to apply such an approach in practice, a more sophisticated infrastructure would be necessary. E.g. the bookkeeping that is necessary to compensate for waiting times that have been too high. (I think they can't be too low). Also, all this still does not guarantee a precisely timed execution. But it may be an option to consider, at least.

like image 35
Marco13 Avatar answered Jan 21 '23 22:01

Marco13