Статьи

Давайте сделаем паузу на микросекунду

Множество тестов в Java-приложениях с низкой задержкой подразумевают необходимость измерения системы при определенной нагрузке. Это требует поддержания постоянной пропускной способности событий в системе, а не нагнетания событий в систему на полной скорости без какого-либо контроля.

Одна из задач, которые мне часто приходится делать, — приостановить поток продюсера на короткий промежуток времени между событиями. Как правило, это количество времени будет состоять из одной цифры микросекунды.

Итак, как вы можете приостановить поток на это количество времени? Большинство разработчиков Java мгновенно думают о Thread.sleep() . Но это не сработает, потому что Thread.sleep() только до миллисекунд, и это на порядок больше, чем количество времени, необходимое для нашей паузы в микросекундах.

Я видел ответ на StackOverflow, указывающий пользователю на TimeUnit.MICROSECONDS.sleep() , чтобы спать менее миллисекунды. Это явно неверно, чтобы процитировать из JavaDoc :

Выполняет Thread.sleep используя эту единицу времени. Это удобный метод, который преобразует временные аргументы в форму, требуемую методом Thread.sleep .

Таким образом, вы не сможете получить паузу в 1 миллисекунду лучше, чем Thread.sleep(1) . (Вы можете доказать это на примере кода ниже).

Причина этого заключается в том, что этот метод приостановки, а именно перевода потока в спящий режим и его пробуждения, никогда не будет быстрым или достаточно точным, чтобы опуститься ниже миллисекунды.

Еще один вопрос, который мы должны поставить на этом этапе, это насколько точно Thread.sleep(1) любом случае? Мы вернемся к этому позже.

Другой вариант, когда мы хотим сделать паузу на микросекунду, это использовать LockSupport.parkNanos(x) . Использование следующего кода для парковки в течение 1 микросекунды на самом деле занимает ~ 10 мкс. Это намного лучше, чем TimeUnit.sleep () / Thread.sleep (), но не совсем подходит для этой цели. После 100 мсек он попадает в тот же парк с разницей всего в 50%.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package nanotime;
 
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
 
/**
 * Created by daniel on 28/10/2015.
 */
public class NanoTimer {
    public static void main(String[] args) throws InterruptedException {
        long[] samples = new long[100_000];
        int pauseInMillis = 1;
 
        for (int i = 0; i < samples.length; i++) {
            long firstTime = System.nanoTime();
            LockSupport.parkNanos(pauseInMicros);
            long timeForNano = System.nanoTime() - firstTime;
            samples[i] = timeForNano;
        }
 
        System.out.printf("Time for LockSupport.parkNanos() %.0f\n", Arrays.stream(samples).average().getAsDouble());
    }
}

Ответом на наши проблемы является использование System.nanoTime () . Занятое ожидание вызова System.nanoTime позволит нам сделать паузу на одну микросекунду. Мы увидим код для этого через секунду, но сначала давайте разберемся с точностью System.nanosecond() . Критически важно, сколько времени требуется для выполнения вызова System.nanoSecond() .

Вот код, который будет делать именно это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
package nanotime;
 
public class NanoTimer {
    public static void main(String[] args) throws InterruptedException {
        long[] samples = new long[1_000_000];
 
        for (int i = 0; i < samples.length; i++) {
            long firstTime = System.nanoTime();
            long timeForNano = System.nanoTime() - firstTime;
            samples[i] = timeForNano;
        }
 
        System.out.printf("Time for call to nano %.0f nanseconds", Arrays.stream(samples).average().getAsDouble());
    }
}

Количество будет варьироваться от одной машины к другой на моем MBP, я получаю ~ 40 наносекунд.

Это говорит нам о том, что мы должны иметь возможность измерять с точностью около 40 наносекунд. Следовательно, измерение в 1 микросекунду (1000 наносекунд) должно быть легко возможным.

Это подход ожидания ожидания, «приостанавливающий» микросекунду:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package nanotime;
 
import java.util.Arrays;
/**
 * Created by daniel on 28/10/2015.
 */
public class NanoTimer {
    public static void main(String[] args) throws InterruptedException {
        long[] samples = new long[100_000];
        int pauseInMicros = 1;
 
        for (int i = 0; i < samples.length; i++) {
            long firstTime = System.nanoTime();
            busyWaitMicros(pauseInMicros);
            long timeForNano = System.nanoTime() - firstTime;
            samples[i] = timeForNano;
        }
 
        System.out.printf("Time for micro busyWait %.0f\n", Arrays.stream(samples).average().getAsDouble());
 
    }
 
    public static void busyWaitMicros(long micros){
        long waitUntil = System.nanoTime() + (micros * 1_000);
        while(waitUntil > System.nanoTime()){
            ;
        }
    }
}

Код ожидает микросекунду, а затем время, как долго он ждал. На моей машине я получаю 1115 наносекунд, что с точностью ~ 90%.

Когда вы дольше ждете, точность увеличивается, 10 микросекунд занимает 10,267, что составляет ~ 97%, а 100 микросекунд — 100,497 наносекунд, что составляет ~ 99,5%.

Как насчет Thread.sleep(1) , насколько это точно?

Вот код для этого:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package nanotime;
 
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
 
/**
 * Created by daniel on 28/10/2015.
 */
public class NanoTimer {
    public static void main(String[] args) throws InterruptedException {
        long[] samples = new long[100_000];
        int pauseInMillis = 1;
 
        for (int i = 0; i < samples.length; i++) {
            long firstTime = System.nanoTime();
            Thread.sleep(pauseInMicros);
            long timeForNano = System.nanoTime() - firstTime;
            samples[i] = timeForNano;
        }
 
        System.out.printf("Time for micro sleep %.0f\n", Arrays.stream(samples).average().getAsDouble());
    }
}

Среднее время в наносекундах для 1 миллисекундного сна составляет 1 295 509. Это только ~ 75% с точностью. Это, вероятно, достаточно хорошо почти для всего, но если вы хотите точную миллисекундную паузу, вам гораздо лучше с занятым ожиданием. Конечно, вы должны помнить, что ожидание занято, по определению, ваш поток будет занят и будет стоить вам процессора.

Таблица результатов

Метод паузы 1us 10us 100us 1000us / 1мс 10,000us / 10мс
TimeUnit.Sleep () 1284,6 1293,8 1295,7 1292,7 11865,3
LockSupport.parkNanos () 8,1 28,4 141,8 1294,3 11834,2
BusyWaiting 1,1 10,1 100,2 1000,2 10000,2

Выводы

  • Если вы хотите сделать паузу менее миллисекунды, вам нужно заняться ожиданием
  • System.nanoSecond () занимает ~ 40 нс
  • Thread.sleep (1) имеет точность только 75%
  • Ожидание более чем на 10 мкс и выше — почти на 100%
  • Оживленное ожидание свяжет процессор
Ссылка: Давайте сделаем паузу на микросекунду от нашего партнера по JCG Даниэля Шая в блоге Rational Java .