В предыдущей статье на межпоточной Latency I было показано , как можно сигнализировать изменение состояния между 2 нитями с менее чем 50ns латентности. Для многих разработчиков написание параллельного кода с использованием блокировок — страшный опыт. Написание параллельного кода с использованием алгоритмов без блокировок, то есть алгоритмов, которые основаны на использовании барьеров памяти и глубокого понимания базовых моделей памяти, может быть совершенно ужасным. Для меня алгоритмы без блокировок / без блокировок — это как игра со взрывчаткой или едкими химическими веществами, если вы не понимаете, что делаете, или проявляете абсолютное уважение, тогда очень плохие вещи могут и, скорее всего, произойдут!
В этой статье я хотел бы проиллюстрировать влияние использования блокировок и возникающую задержку, которую они могут наложить на ваши проекты. Я хочу использовать алгоритм, очень похожий на тот, который использовался в моей предыдущей статье о задержке между потоками, чтобы проиллюстрировать эффект пинг-понга при передаче управления вперед и назад между двумя потоками. В этом случае, вместо того, чтобы использовать пару изменчивых переменных, я буду использовать пару переменных условий, чтобы сигнализировать об изменении состояния, чтобы управление могло передаваться взад и вперед.
Код
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static java.lang.System.out; public final class LockedSignallingLatency { private static final int ITERATIONS = 10 * 1000 * 1000; private static final Lock lock = new ReentrantLock(); private static final Condition sendCondition = lock.newCondition(); private static final Condition echoCondition = lock.newCondition(); private static long sendValue = -1L; private static long echoValue = -1L; public static void main(final String[] args) throws Exception { final Thread sendThread = new Thread(new SendRunner()); final Thread echoThread = new Thread(new EchoRunner()); final long start = System.nanoTime(); echoThread.start(); sendThread.start(); sendThread.join(); echoThread.join(); final long duration = System.nanoTime() - start; out.printf("duration %,d (ns)\n", duration); out.printf("%,d ns/op\n", duration / (ITERATIONS * 2L)); out.printf("%,d ops/s\n", (ITERATIONS * 2L * 1000000000L) / duration); } public static final class SendRunner implements Runnable { @Override public void run() { for (long i = 0; i < ITERATIONS; i++) { lock.lock(); try { sendValue = i; sendCondition.signal(); } finally { lock.unlock(); } lock.lock(); try { while (echoValue != i) { echoCondition.await(); } } catch (final InterruptedException ex) { break; } finally { lock.unlock(); } } } } public static final class EchoRunner implements Runnable { @Override public void run() { for (long i = 0; i < ITERATIONS; i++) { lock.lock(); try { while (sendValue != i) { sendCondition.await(); } } catch (final InterruptedException ex) { break; } finally { lock.unlock(); } lock.lock(); try { echoValue = i; echoCondition.signal(); } finally { lock.unlock(); } } } } }
Результаты
64-разрядной версии Windows 7 Professional — Oracle JDK 1.6.0 — Nehalem 2,8 ГГц
$ start /AFFINITY 0x14 /B /WAIT java LockedSignallingLatency duration 41,649,616,343 (ns) 2,082 ns/op 480,196 ops/s $ java LockedSignallingLatency duration 73,789,456,491 (ns) 3,689 ns/op 271,041 ops/s
Linux Fedora Core 15 64-разрядная версия — Oracle JDK 1.6.0 — Sandybridge 2,0 ГГц
$ taskset -c 2,4 java LockedSignallingLatency duration 47,209,549,484 (ns) 2,360 ns/op 423,643 ops/s $ java LockedSignallingLatency duration 336,168,489,093 (ns) 16,808 ns/op 59,493 ops/s
Наблюдения
Выше приведен типичный набор результатов, которые я видел в середине диапазона от нескольких прогонов. Есть пара интересных наблюдений, на которых я хотел бы остановиться.
Во-первых, односторонняя задержка для оповещения об изменении в значительной степени совпадает с тем, что считается современным уровнем техники для сетевых перескоков между узлами через коммутатор. Можно получить ~ 1 μ s задержка с InfiniBand и менее 5 μ s с 10GigE и IP стеки пользовательского пространства, Это на 3 порядка больше, чем то, что я проиллюстрировал в предыдущей статье, используя только барьеры памяти для передачи сигналов между потоками. Эта стоимость возникает из-за того, что ядро должно участвовать в арбитраже между потоками для блокировки, а затем управлять планированием пробуждения потоков, когда сигнализируется условие.
Во-вторых, влияние становится очевидным, если позволить ОС выбирать, на какие ЦП запланированы потоки, а не закреплять их вручную. Я наблюдал эту же проблему во многих случаях использования, когда Linux в конфигурации по умолчанию для своего планировщика будет сильно влиять на производительность системы с низкой задержкой, планируя потоки на разных ядрах, что приводит к загрязнению кэша. По умолчанию Windows лучше справляется с этой задачей.
Недавно у меня была интересная дискуссия с Клифф Кликом об использовании условных переменных и их стоимости. Он указал на проблему, которую он видел. Если вы посмотрите на случай, когда спящий поток получает сигнал в пределах блокировки, он запускается и обнаруживает, что не может получить блокировку, потому что сигнальный поток уже имеет блокировку, поэтому он возвращается в спящий режим, пока сигнальный поток не освободит заблокировать, тем самым вызывая больше работы, чем необходимо. Современные планировщики выиграют, если будут лучше осведомлены о механизмах связи между потоками, чтобы иметь более эффективную логику определения местоположения и переупорядочения. По мере того, как мы будем работать параллельно и параллельно, наши планировщики должны лучше понимать механизмы IPC .
Вывод
При проектировании системы с низкой задержкой крайне важно избегать использования блокировок и переменных условий для основных потоков транзакций. Неблокирующие или безблокировочные алгоритмы являются ключом к достижению сверхнизкой задержки, но может оказаться очень трудным доказать правильность. Я не рекомендовал бы разрабатывать алгоритмы без блокировки для бизнес-логики, но они могут быть очень эффективно использованы для низкоуровневых компонентов инфраструктуры. Бизнес-логику лучше всего выполнять в отдельных потоках, следуя принципу единого писателя из моей предыдущей статьи.
От http://mechanical-sympathy.blogspot.com/2011/11/locks-condition-variables-latency.html