Почему блокировка вместо зацикливания?
По каким причинам написание следующего фрагмента кода считается плохой практикой?
while (someList.isEmpty()) {
try {
Thread.currentThread().sleep(100);
}
catch (Exception e) {}
}
// Do something to the list as soon as some thread adds an element to it.
Для меня выбор произвольного значения для сна не является хорошей практикой, и я бы использовал BlockingQueue
в этой ситуации, но я хотел бы знать, если есть несколько причин, почему не следует писать такой код.
6 ответов
Он накладывает среднюю задержку в 50 миллисекунд до того, как событие активируется, и он просыпается 10 раз в секунду, когда событие не обрабатывается. Если ни одна из этих вещей не имеет особого значения, то это просто не элегантно.
Цикл является отличным примером того, чего не следует делать.;)
Thread.currentThread().sleep(100);
Нет необходимости получать currentThread(), так как это статический метод. Это так же, как
Thread.sleep(100);
catch (Exception e) {}
Это очень плохая практика. Настолько плохо, что я бы не советовал вам это даже в примерах, поскольку кто-то может скопировать код. Значительная часть вопросов на этом форуме будет решена путем распечатки и прочтения данного исключения.
You don't need to busy wait here. esp. when you expect to be waiting for such a long time. Busy waiting can make sense if you expect to be waiting a very very short amount of time. e.g.
// From AtomicInteger
public final int getAndSet(int newValue) {
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}
Как вы можете видеть, это должно быть довольно редко, когда этот цикл должен повторяться более одного раза, и с экспоненциальной вероятностью обходить его много раз. (В реальном приложении, а не в микро-бенчмарке) Этот цикл может составлять всего 10 нс, что не является большой задержкой.
Это может подождать 99 мс без необходимости. Скажем, продюсер добавляет запись через 1 мс, он долго ничего не ждал.
Решение проще и понятнее.
BlockingQueue<E> queue =
E e = queue.take(); // blocks until an element is ready.
Список / очередь будет меняться только в другом потоке, и гораздо более простая модель для управления потоками и очередями заключается в использовании ExecutorService
ExecutorService es =
final E e =
es.submit(new Runnable() {
public void run() {
doSomethingWith(e);
}
});
Как видите, вам не нужно работать с очередями или потоками напрямую. Вам просто нужно сказать, что вы хотите, чтобы пул потоков делал.
Есть много причин не делать этого. Во-первых, как вы заметили, это означает, что может быть большая задержка между временем возникновения события, на которое должен ответить поток, и фактическим временем ответа, поскольку поток может находиться в спящем режиме. Во-вторых, поскольку в любой системе очень много разных процессоров, если вам приходится постоянно отрывать важные потоки от процессора, чтобы они могли указать потоку перейти в спящий режим в другой раз, вы уменьшаете общий объем полезной работы, выполняемой системой. и увеличить энергопотребление системы (что имеет значение в таких системах, как телефоны или встроенные устройства).
Вы также вводите условия гонки для своего класса. если бы вы использовали очередь блокировки вместо обычного списка - поток заблокировал бы, пока в списке не появится новая запись. В вашем случае второй поток может поместить и получить элемент из списка, когда ваш рабочий поток спит, и вы даже не заметите.
Я не могу прямо добавить к отличным ответам, данным Дэвидом, templatetypedef и т. Д. - если вы хотите избежать задержки между потоками и потери ресурсов, не делайте связи между потоками с циклами sleep().
Упреждающее планирование / диспетчеризация:
На уровне ЦП прерывания являются ключом. ОС ничего не делает до тех пор, пока не произойдет прерывание, которое приведет к вводу ее кода. Обратите внимание, что в терминах ОС прерывания бывают двух видов - "настоящие" аппаратные прерывания, которые вызывают запуск драйверов, и "программные прерывания" - это системные вызовы ОС из уже запущенных потоков, которые потенциально могут вызвать набор запущенных потоков. изменить. Клавиши, движения мыши, сетевые карты, диски, ошибки страниц - все это вызывает аппаратные прерывания. Функции wait и signal и sleep() относятся ко второй категории. Когда аппаратное прерывание вызывает запуск драйвера, драйвер выполняет любое аппаратное управление, для которого он предназначен. Если драйверу нужно сообщить ОС, что какой-то поток должен быть запущен (возможно, дисковый буфер теперь заполнен и нуждается в обработке), ОС предоставляет механизм ввода, который драйвер может вызвать, вместо непосредственного выполнения прерывания. Вернись сам (важно!).
Прерывания, подобные приведенным выше примерам, могут сделать потоки, которые ожидали, готовы к запуску и / или могут заставить поток, который работает, перейти в состояние ожидания. После обработки кода прерывания ОС применяет свой алгоритм (ы) планирования, чтобы решить, совпадает ли набор потоков, которые работали до прерывания, с набором, который должен теперь выполняться. Если они есть, ОС просто прерывает-возвращает, если нет, ОС должна выгрузить один или несколько запущенных потоков. Если ОС необходимо выгрузить поток, работающий на ядре ЦП, но не обрабатывающий прерывание, она должна получить контроль над этим ядром ЦП. Это осуществляется с помощью "реального" аппаратного прерывания - межпроцессорный драйвер ОС устанавливает аппаратный сигнал, который жестко прерывает ядро, на котором выполняется поток, который должен быть прерван.
Когда поток, который должен быть прерван, вводит код ОС, ОС может сохранить полный контекст для потока. Некоторые из регистров уже будут сохранены в стек потока с помощью записи прерывания, поэтому сохранение указателя стека потока будет эффективно "сохранять" все эти регистры, но ОС обычно требуется делать больше, например. может потребоваться очистка кешей, может потребоваться сохранение состояния FPU, и в случае, когда запускаемый новый поток принадлежит процессу, отличному от того, который должен быть выгружен, регистры защиты управления памятью должны быть заменены, Обычно ОС переключается из стека с прерванными потоками в частный стек ОС как можно быстрее, чтобы избежать наложения требований стека ОС на каждый стек потоков.
После сохранения / сохранения контекста / ОС операционная система может "заменить" расширенный контекст / ы на новые потоки, которые должны быть запущены. Теперь ОС может, наконец, загрузить указатель стека для новых потоков и выполнить возврат прерываний, чтобы запустить новые готовые потоки.
ОС тогда вообще ничего не делает. Работающие потоки работают до тех пор, пока не произойдет другое прерывание (жесткое или мягкое).
Важные моменты:
1) Ядро ОС следует рассматривать как большой обработчик прерываний, который может принять решение о прерывании-возврате в другой набор потоков, отличающийся от прерываемых.
2) ОС может получить контроль и при необходимости остановить любой поток в любом процессе, независимо от того, в каком состоянии он находится или на каком ядре он может работать.
3) Упреждающее планирование и диспетчеризация порождает все проблемы с синхронизацией и т. Д., Которые публикуются на этих форумах. Большим преимуществом является быстрое реагирование на уровне потоков на жесткие прерывания. Без этого все эти высокопроизводительные приложения, которые вы запускаете на своем компьютере - потоковое видео, быстрые сети и т. Д., Были бы практически невозможны.
4) Таймер ОС - это лишь одно из большого набора прерываний, которые могут изменить набор запущенных потоков. "Time-Slicing" (тьфу - я ненавижу этот термин), между готовыми потоками происходит только тогда, когда компьютер перегружен, т.е. набор готовых потоков больше, чем количество ядер ЦП, доступных для их запуска. Если какой-либо текст, предназначенный для объяснения планирования ОС, упоминает "квантование времени" перед "прерываниями", это может вызвать больше путаницы, чем объяснения. Прерывание таймера является только "специальным" в том смысле, что многие системные вызовы имеют тайм-ауты для резервного копирования своей основной функции (ОК, для режима сна (), тайм-аут является основной функцией:).
Чтобы добавить к другим ответам, у вас также есть условие гонки, если у вас есть несколько потоков, удаляющих элементы из очереди:
- очередь пуста
- поток A помещает элемент в очередь
- поток B проверяет, пуста ли очередь; это не
- поток C проверяет, пуста ли очередь; это не
- поток B берет из очереди; успех
- поток C берет из очереди; отказ
Вы можете справиться с этим атомарно (в пределах synchronized
блок) проверка, пуста ли очередь, и, если нет, извлечение элемента из нее; теперь ваша петля выглядит просто уродливой
T item;
while ( (item = tryTake(someList)) == null) {
try {
Thread.currentThread().sleep(100);
}
catch (InterruptedException e) {
// it's almost never a good idea to ignore these; need to handle somehow
}
}
// Do something with the item
synchronized private T tryTake(List<? extends T> from) {
if (from.isEmpty()) return null;
T result = from.remove(0);
assert result != null : "list may not contain nulls, which is unfortunate"
return result;
}
или вы могли бы просто использовать BlockingQueue
,