Używam BlockingQueue: s (próbuję obu ArrayBlockingQueue i LinkedBlockingQueue) do przekazywania obiektów między różnymi wątkami w aplikacji, nad którą obecnie pracuję. Wydajność i opóźnienie są stosunkowo ważne w tej aplikacji, więc byłem ciekawy, ile czasu zajmuje przesłanie obiektów między dwoma wątkami za pomocą BlockingQueue. Aby to zmierzyć, napisałem prosty program z dwoma wątkami (jeden konsument i jeden producent), w którym pozwoliłem producentowi przekazać znacznik czasu (wzięty przy użyciu System.nanoTime()) do konsumenta, patrz kod poniżej.Java BlockingQueue opóźnienie wysokie w systemie Linux
Przypominam sobie, że przeczytałem gdzieś na jakimś forum, że zajęło to około 10 mikrosekund dla kogoś, kto próbował tego (nie wiem na jakim systemie operacyjnym i sprzęcie, który był włączony), więc nie byłem zbyt zaskoczony, gdy zajęło to ~ 30 mikrosekund dla mnie na moim pudełku Windows 7 (procesor Intel E7500 core 2 duo, 2,93 GHz), podczas gdy w tle jest wiele innych aplikacji. Byłem jednak bardzo zaskoczony, gdy wykonałem ten sam test na znacznie szybszym serwerze Linux (dwa czterordzeniowe procesory Intel X5677 3,46 GHz, z systemem Debian 5 z jądrem 2.6.26-2-amd64). Spodziewałem się, że opóźnienie będzie mniejsze niż w moim oknie z oknami, ale przeciwnie było znacznie wyższe - ~ 75 - 100 mikrosekund! Oba testy zostały wykonane przy użyciu Sun Hotspot JVM w wersji 1.6.0-23.
Czy ktoś inny wykonał podobne testy z podobnymi wynikami w systemie Linux? Czy ktoś może wiedzieć, dlaczego jest tak wolniejszy w Linuksie (z lepszym sprzętem), czy to możliwe, że przełączanie wątków jest po prostu znacznie wolniejsze w Linuksie w porównaniu z oknami? Jeśli tak jest, wygląda na to, że okna są lepiej dostosowane do niektórych aplikacji. Jakąkolwiek pomoc w zrozumieniu względnie wysokich wartości jest bardzo cenna.
Edit:
Po Komentarz od DaveC, ja też zrobiłem test, gdzie ograniczony JVM (na maszynie Linux) z pojedynczym rdzeniem (to znaczy wszystkie wątki uruchomione na tym samym rdzeniu). To znacznie zmieniło wyniki - opóźnienie spadło poniżej 20 mikrosekund, czyli było lepsze niż wyniki na komputerze z systemem Windows. Zrobiłem też kilka testów, w których ograniczyłem wątek producenta do jednego rdzenia i wątek konsumencki do drugiego (próbując oba mieć je na tym samym gnieździe i na różnych gniazdach), ale to nie pomagało - opóźnienie było wciąż ~ 75 mikrosekundy. Przy okazji, ta aplikacja testowa jest praktycznie wszystkim, co używam na maszynie podczas wykonywania testu.
Czy ktoś wie, czy te wyniki mają sens? Czy naprawdę powinno być o wiele wolniej, jeśli producent i konsument działają na różnych rdzeniach? Wszelkie dane wejściowe są naprawdę doceniane.
Edited ponownie (6 stycznia):
eksperymentowałem z różnymi zmianami w kodzie i uruchomiony środowiska:
uaktualnieniu jądra do 2.6.36.2 (od 2.6.26.2). Po aktualizacji jądra zmierzony czas zmienił się na 60 mikrosekund przy bardzo małych zmianach, od 75-100 przed aktualizacją. Ustawienie powinowactwa procesora dla wątków producenta i konsumenta nie miało żadnego skutku, z wyjątkiem ograniczania ich do tego samego rdzenia. Podczas pracy na tym samym rdzeniu zmierzone opóźnienie wynosiło 13 mikrosekund.
W oryginalnym kodzie kazałem producentowi iść spać na 1 sekundę pomiędzy każdą iteracją, aby dać konsumentowi wystarczająco dużo czasu na obliczenie upływu czasu i wydrukowanie go na konsoli. Jeśli usuniemy połączenie do Thread.sleep() i zamiast tego pozwól zarówno producentowi, jak i konsumentowi wywołać barrier.await() w każdej iteracji (konsument wywoła to po wydrukowaniu czasu, który upłynął do konsoli), zmierzone opóźnienie zostanie zmniejszone z 60 mikrosekund do poniżej 10 mikrosekund. W przypadku uruchamiania wątków na tym samym rdzeniu opóźnienie wynosi poniżej 1 mikrosekundy. Czy ktoś może wyjaśnić, dlaczego tak znacznie zmniejszyło to opóźnienie?Moim pierwszym przypuszczeniem było to, że zmiana spowodowała, że producent wywołał funkcję queue.put() przed konsumentem o nazwie queue.take(), więc konsument nigdy nie musiał blokować, ale po zabawie ze zmodyfikowaną wersją ArrayBlockingQueue, znalazłem to przypuszczenie jest fałszywe - konsument faktycznie blokował. Jeśli masz inne domysły, daj mi znać. (Btw, jeśli pozwolę producentowi wywołać zarówno Thread.sleep() jak i barrier.await(), opóźnienie pozostaje na poziomie 60 mikrosekund).
Próbowałem także inne podejście - zamiast wywoływać queue.take(), zadzwoniłem do kolejki.poll() z limitem czasu 100 mikropów. Zmniejszyło to średnie opóźnienie do poniżej 10 mikrosekund, ale jest oczywiście o wiele bardziej obciążające procesor (ale prawdopodobnie mniej intensywnego procesora, który jest zajęty czekaniem?).
Zmieniano ponownie (10 stycznia) - Problem rozwiązany:
ninjalj zasugerował, że opóźnienie ~ 60 mikrosekund było spowodowane procesor mający obudzić się z głębszych stanów uśpienia - i był całkowicie w porządku! Po wyłączeniu stanów C w systemie BIOS opóźnienie zostało zmniejszone do < 10 mikrosekund. To tłumaczy, dlaczego mam o wiele lepsze opóźnienie w punkcie 2 powyżej - kiedy wysyłałem obiekty częściej, procesor był wystarczająco zajęty, aby nie przejść do głębszych stanów snu. Wielkie podziękowania dla wszystkich, którzy poświęcili czas na przeczytanie mojego pytania i podzielili się tutaj swoimi przemyśleniami!
...
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CyclicBarrier;
public class QueueTest {
ArrayBlockingQueue<Long> queue = new ArrayBlockingQueue<Long>(10);
Thread consumerThread;
CyclicBarrier barrier = new CyclicBarrier(2);
static final int RUNS = 500000;
volatile int sleep = 1000;
public void start() {
consumerThread = new Thread(new Runnable() {
@Override
public void run() {
try {
barrier.await();
for(int i = 0; i < RUNS; i++) {
consume();
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
consumerThread.start();
try {
barrier.await();
} catch (Exception e) { e.printStackTrace(); }
for(int i = 0; i < RUNS; i++) {
try {
if(sleep > 0)
Thread.sleep(sleep);
produce();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void produce() {
try {
queue.put(System.nanoTime());
} catch (InterruptedException e) {
}
}
public void consume() {
try {
long t = queue.take();
long now = System.nanoTime();
long time = (now - t)/1000; // Divide by 1000 to get result in microseconds
if(sleep > 0) {
System.out.println("Time: " + time);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
QueueTest test = new QueueTest();
System.out.println("Starting...");
// Run first once, ignoring results
test.sleep = 0;
test.start();
// Run again, printing the results
System.out.println("Starting again...");
test.sleep = 1000;
test.start();
}
}
wypróbowałeś test na Linuksie ograniczając jvm do używania tylko jednego procesora? może pomóc określić, gdzie upływa czas. – DaveC
Interesujące - próbowałem ograniczyć go do konkretnego procesora, uruchamiając aplikację za pomocą polecenia "taskset 0x00000001 java QueueTest", a opóźnienie zostało zredukowane z około 75-100 do ~ 20 mikrosekund! Nie jestem pewien, czy rozumiem, ale ... – Johan
@Johan: Czy te razy raportujesz to samo w wielu iteracjach? CyklicBarrier służy do koordynowania wątków pracujących na niezależnych zadaniach. Twoje zadania nie są jednak niezależne. Masz zarówno producenta, jak i Konsument czeka na barierce, a następnie (gdy oba wątki osiągną punkt zapory) zasadniczo rozpoczynają synchronizację w kolejce blokującej. Można zobaczyć przeplot różnego rodzaju kombinacji harmonogramów raportujących różne opóźnienia. – Cratylus