Почему блокировка вместо зацикливания? - PullRequest
6 голосов
/ 12 января 2012

По каким причинам написание следующего фрагмента кода считается плохой практикой?

  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 ]

6 голосов
/ 12 января 2012

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

1 голос
/ 12 января 2012

Цикл является отличным примером того, чего не следует делать. ;)


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);
   }
});

Как видите, вам не нужно напрямую работать с очередями или потоками. Вам просто нужно сказать, что вы хотите от пула потоков.

1 голос
/ 12 января 2012

Есть много причин, чтобы этого не делать. Во-первых, как вы заметили, это означает, что может быть большая задержка между временем возникновения события, на которое должен ответить поток, и фактическим временем ответа, поскольку поток может находиться в спящем режиме. Во-вторых, поскольку в любой системе очень много разных процессоров, если вам приходится постоянно отрывать важные потоки от процессора, чтобы они могли указать потоку перейти в спящий режим в другой раз, вы уменьшаете общий объем полезной работы, выполняемой системой. и увеличить энергопотребление системы (что имеет значение в таких системах, как телефоны или встроенные устройства).

0 голосов
/ 12 января 2012

Я не могу напрямую добавить к отличным ответам, данным Дэвидом, templatetypedef и т. Д. - если вы хотите избежать задержек между потоками и траты ресурсов, не делайте обменов между потоками с циклами sleep ().

Упреждающее планирование / диспетчеризация:

На уровне ЦП прерывания являются ключом. ОС ничего не делает до тех пор, пока не произойдет прерывание, которое приведет к вводу ее кода. Обратите внимание, что в терминах ОС прерывания бывают двух видов - «настоящие» аппаратные прерывания, которые вызывают запуск драйверов, и «программные прерывания» - это системные вызовы ОС из уже запущенных потоков, которые потенциально могут вызвать набор запущенных потоков. изменить. Клавиши, движения мыши, сетевые карты, диски, ошибки страниц - все это вызывает аппаратные прерывания. Функции wait и signal и sleep () относятся ко второй категории. Когда аппаратное прерывание вызывает запуск драйвера, драйвер выполняет любое аппаратное управление, для которого он предназначен. Если драйверу нужно сообщить ОС, что какой-то поток должен быть запущен (возможно, дисковый буфер теперь заполнен и нуждается в обработке), ОС предоставляет механизм ввода, который драйвер может вызвать, вместо непосредственного выполнения прерывания. Вернись, (важно!).

Прерывания, подобные приведенным выше примерам, могут сделать ожидающие выполнение потоки готовыми к запуску и / или могут заставить выполняющийся поток войти в состояние ожидания. После обработки кода прерывания ОС применяет свой алгоритм (ы) планирования, чтобы решить, совпадает ли набор потоков, которые работали до прерывания, с набором, который должен теперь выполняться. Если это так, ОС просто прерывает-возвращает, если нет, ОС должна выгрузить один или несколько запущенных потоков. Если ОС необходимо выгрузить поток, работающий на ядре ЦП, но не обрабатывающий прерывание, она должна получить контроль над этим ядром ЦП. Это осуществляется с помощью «реального» аппаратного прерывания - межпроцессорный драйвер ОС устанавливает аппаратный сигнал, который жестко прерывает ядро, на котором выполняется поток, который должен быть прерван.

Когда поток, который должен быть прерван, вводится в код ОС, ОС может сохранить полный контекст для потока. Некоторые из регистров уже будут сохранены в стек потока с помощью записи прерывания, поэтому сохранение указателя стека потока будет эффективно «сохранять» все эти регистры, но ОС обычно требуется делать больше, например. может потребоваться очистка кешей, может потребоваться сохранение состояния FPU, и, в случае, когда новый поток, который должен быть запущен, принадлежит процессу, отличному от того, который должен быть прерван, регистры защиты управления памятью должны быть заменены , Обычно ОС переключается со стека прерванных потоков на частный стек ОС как можно быстрее, чтобы избежать наложения требований стека ОС на каждый стек потоков.

После сохранения / сохранения контекста / ОС операционная система может «заменить» расширенный контекст / ы на новые потоки, которые должны быть запущены. Теперь ОС может наконец загрузить указатель стека для новых потоков и выполнить возврат прерываний, чтобы запустить новые готовые потоки.

Тогда ОС вообще ничего не делает. Работающие потоки работают, пока не произойдет другое прерывание (жесткое или мягкое).

Важные моменты:

1) Ядро ОС следует рассматривать как большой обработчик прерываний, который может принять решение о прерывании-возврате в другой набор потоков, отличающийся от прерванных.

2) ОС может получить контроль и при необходимости остановить любой поток в любом процессе, независимо от того, в каком он состоянии или на каком ядре он работает.

3) Упреждающее планирование и диспетчеризация создает все проблемы с синхронизацией и т. Д., Которые публикуются на этих форумах. Большим преимуществом является быстрое реагирование на уровне потоков на жесткие прерывания. Без этого все эти высокопроизводительные приложения, которые вы запускаете на своем компьютере - потоковое видео, быстрые сети и т. Д., Были бы практически невозможны.

4) Таймер ОС - это лишь одно из большого набора прерываний, которое может изменить набор запущенных потоков. «Разрезание по времени» (тьфу - я ненавижу этот термин) между готовыми потоками происходит только тогда, когда компьютер перегружен, т.е. набор готовых потоков больше, чем количество ядер ЦП, доступных для их запуска. Если какой-либо текст, предназначенный для объяснения планирования ОС, упоминает «квантование времени» перед «прерываниями», это может вызвать больше путаницы, чем объяснения. Прерывание таймера является только «особым» в том смысле, что многие системные вызовы имеют тайм-ауты для резервного копирования своей основной функции (ОК, для режима сна () тайм-аут является основной функцией:).

0 голосов
/ 12 января 2012

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

  1. очередь пуста
  2. поток A помещает элемент в очередь
  3. поток B проверяет, пуста ли очередь; это не
  4. поток C проверяет, пуста ли очередь; это не
  5. поток B берет из очереди; успех
  6. поток 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.

0 голосов
/ 12 января 2012

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

...