Autor Wątek: Server: aplikacja wielowątkowa  (Przeczytany 4512 razy)

Offline Pirs01

  • Użytkownik

# Kwiecień 12, 2007, 20:23:41
Problemy które tutaj omówię są dosyć ogólne i dotyczą konstruowania aplikacji serverowej z użyciem wielowątkowości. Nie wiem czy problemy te wynikają z mojej niewiedzy, złego podejścia do zagadnienia czy słabych punktów języka, w każdym razie:

Próbuję napisać server gry. Korzystam z "nonblocking I/O" w oparciu o artykuł: http://www-128.ibm.com/developerworks/java/library/j-javaio/ Nie wnikając w szczegóły opisanej w tym artykule techniki, powiem tylko że nie do końca jest ona "nonblocking". Prgoram wykorzystujący tą technikę blokuje się gdy zaczyna nasłuchiwać nowego połączenia tak więc w moim serverze obsługą nawiązanych już połączeń mogę się zając w "głównym wątku" ale na potrzebę nasłuchu nowych połączeń muszę utworzyć oddzielny wątek. Zgodnie z moją koncepcją server składałby się z trzech wątków:

1.Wątek nasłuchu nowych połączeń: nasłuchujue nowego połączenia, akceptuje je i przekazuje nawiązane połączenie do wątku głównego.
2.Wątek główny: Odbiera informacje od klientów, przetwarza je i odsyłą spowrotem.
3.Wątek połączenia z bazą danych: Server ma się łączyć i utrzymywać komunikację z bazą SQL która będzie się znajdować na innej maszynie. Narazie nie zagłębiałem się w to zagadnienie ale podejrzewam że jest tam masa operacji blokujących więc przydzielam temu zagadnieniu oddzielny wątek.

Z takiego podziału wynika że różne wątki muszą operować na tych samych danych np: wątek 1 po nawiązaniu nowego połączenia musi dodać nowego klienta do jakiejś umownej listyKlientów, natomiast wątek 2 tą samą listę będzie czytał w celu przetwarzania danych. Ponieważ taka czynność może grozić zepsuciem danych, z pomocą przychodzi nam synchornizacja. W przykładach w oparciu o które omówiony jest rozdział "Wielowątkowość" w książce "JAVA 2 - Techniki Zaawansowane" synchronizowana jest jedna metoda operująca na jakiś tam danych. Metoda ta jest wywoływana w wielu wątkach i dlatego deklarowana jest ze słowem kluczowym synchronized albo tworzona w niej jest jawna blokada. No i problem z głowy. Jednak cała ta idea synchronizacji zdaje się nie mieć zastosowania w moim przypadku. Wątek 1 wywoływać będzie przykładową metodą DodajKlientaDoListy(Klient klient) a wątek 2 wywoływać będzie zupełnie inną metodę (np: DaneOKliencie PrzeczytajDaneOKliencie(Klient klient) ) która operować będzie na tych samych danych, tak więc to że użyję przy deklaracji obydwu metod słowa kluczowego synchronized nie uchroni mnie w żaden sposób przed popsuciem danych. No i cała koncepcja synchronizacji pada.
Pierwsze moje pytanie to: Czy wyciągnięty powyżej wniosek jest prawidłowy?
Jesli tak to drugie pytanie brzmi: Jak sobie poradzić z tym problemem?

No i jeszcze jedno zagadnienie:
Załóżmy że mój server składa się tylko z watków numer 1 i 2. Wątek numer 1 ma to do siebie że musi wywołać metodę blokującą żeby zaakceptować połączenie i dzieje się tak nawet gdy żadne połączenie nie oczekuje na zaakceptowanie bo nie da się sprawdzić czy jakieś połączenie czeka na zaakceptowanie bez wywołania tej samej metody blokującej. W praktyce oznacza to że wątek ten będzie się po zaakceptowaniu każdego nowego połaczenia zawieszał aż do momentu nadejścia nowego połączenia. I o ile mi wiadomo wątek taki będzie działał dopuki nie wywłaszczy go system operacyjny stwierdzając że działa on już za długo. Natomiast wątek numer 2 będzie zawierał zdecydowaną większość kodu servera i będzie zdecydowanie bardziej czasochłonny. Tak więc chcąc mu dać możliwie dużo czasu na działanie nie będe go zawieszał po każdej pętli tylko pozwole mu działać aż wywłaszczy go system. Zakładam że system wywłaszczać będzie każdy z wątków po takim samym czasie czyli procesor będzie poświęcał tyle samo czasu wątkowi numer 1 jaki i wątkowi numer 2. Ale biorąc pod uwagę że 95% kodu znajduje się w wątku numer 2 to w praktyce oznaczać to będzie że przez połowę czasu pracy servera procesor pracuje na pełnych obrotach a przez połowę czasu nie robi nic czekając aż zakończy swoje działanie jedna metoda blokujaca, innymi słowy mój server będzie miał w praktyce dostęp tylko do połowy mocy obliczeniowej komputera. Czy tak rzeczywiście będzie? Czy można jakoś zmusić wątek nr 1 do szybszego wywłaszczenia? Czy można w jakiś sposób stwierdzić że wątek numer 1 wykonuje obecnie metodę blokującą i zmienić status wątku? A może problem ten wynika z mojego błędnego podejścia do zagadnienia?

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Goliatus

  • Użytkownik
    • Warsztat - tworzenie gier

# Kwiecień 12, 2007, 21:16:59
A to znasz?

Kod: (java) [Zaznacz]
Object obiekt;

void metoda() {
...
  synchronised(obiekt) {

  }
...
}

to odnośnie pierwszego zagadnienia, a co do drugiego to nie wiem(jeszcze) :)
« Ostatnia zmiana: Kwiecień 12, 2007, 21:33:13 wysłana przez Goliatus »

Offline Pirs01

  • Użytkownik

# Kwiecień 12, 2007, 22:08:00
Domyślam się że w ten sposób tworzy się niejawna blokadę do całego obiektu? jego wszystkich pól i metod?

Czyli wystarczy że wszsytkie  "ogólne dane" na któych będzie operowała więcej niż jedna metoda zsynchronizowana powinienem umieść w takim właśnie obiekcie i potem zakładać na niego blokadę?

DaneOGrze daneOGrze;

String CzytajDaneOGrze()//metoda wywoływana przez wątek A
{
   synchronized (daneOGrze)
  {
     //blok krytyczny
  }
}

void ZapiszDaneOGrze(String string)//metoda wywoływana przez wątek B
{
   synchronized (daneOGrze)
  {
     //blok krytyczny
  }
}
Dobrze zrozumiałem?
No i jeszcze ta druga sprawa: jak się ustrzec przed wątkiem który prawie nic nie robi (tylko czeka na wykonanie metody blokującej ) zato pożera mnóstwo czasu bo muszę czekać aż system wywłaszczy ten wątek i odzyskam dostęp do procesora w wątku głównym odpowiedzialnym za przetwarzanie danych? No chyba że ten problem jest sztuczny, bo nie upieram się że tak właśnie się dzieje tylko się domyślam jak fatalnie może to wpłynąc na wydajność.

Offline parmezan

  • Użytkownik

# Kwiecień 12, 2007, 22:20:08
Cytuj
Wątek 1 wywoływać będzie przykładową metodą DodajKlientaDoListy(Klient klient) a wątek 2 wywoływać będzie zupełnie inną metodę (np: DaneOKliencie PrzeczytajDaneOKliencie(Klient klient) ) która operować będzie na tych samych danych, tak więc to że użyję przy deklaracji obydwu metod słowa kluczowego synchronized nie uchroni mnie w żaden sposób przed popsuciem danych. No i cała koncepcja synchronizacji pada.
To zależy czy obie te metody są wywoływane na rzecz tego samego obiektu. Jeśli wywołasz obie te metody dla tego samego obiektu, to będą one synchronizowane. Taki kod:
Kod: (java) [Zaznacz]
    public synchronized void met() {
        ...
    }
jest odpowiednikiem czegoś takiego:
Kod: (java) [Zaznacz]
    public void metoda2() {
        synchronized(this) {
            ...
        }
    }

Cytuj
No i jeszcze ta druga sprawa: jak się ustrzec przed wątkiem który prawie nic nie robi (tylko czeka na wykonanie metody blokującej ) zato pożera mnóstwo czasu bo muszę czekać aż system wywłaszczy ten wątek i odzyskam dostęp do procesora w wątku głównym odpowiedzialnym za przetwarzanie danych? No chyba że ten problem jest sztuczny, bo nie upieram się że tak właśnie się dzieje tylko się domyślam jak fatalnie może to wpłynąc na wydajność.
Zawieszenie jednego wątku na operacji blokującej to dość powszechna praktyka i raczej nie powoduje to marnowana zasobów.

Offline Pirs01

  • Użytkownik

# Kwiecień 12, 2007, 22:31:04
OK, to chyba wszystko co muszę wiedzieć. Dzięki chłopaki!

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

# Kwiecień 12, 2007, 22:58:14
Cytuj
Prgoram wykorzystujący tą technikę blokuje się gdy zaczyna nasłuchiwać nowego połączenia tak więc w moim serverze obsługą nawiązanych już połączeń mogę się zając w "głównym wątku" ale na potrzebę nasłuchu nowych połączeń muszę utworzyć oddzielny wątek.
A nie prościej przestawić gniazdo nasłuchujące w tryb nieblokujący? Ja tak robiłem u siebie i wszystko śmiga aż miło. :)

bies

  • Gość
# Kwiecień 13, 2007, 00:06:18
To zależy czy obie te metody są wywoływane na rzecz tego samego obiektu. Jeśli wywołasz obie te metody dla tego samego obiektu, to będą one synchronizowane.

Pirs01: zwróć uwagę na powyższe, bo zapominając o takim zachowaniu można często narobić sobie kłopotów (trzeba pamiętać, że to co jest w synchronized() to obiekt blokady a synchronizacja będzie dotyczyła tylko tych wątków w których obiekt blokady jest ten sam). Poniżej masz przykład prostego do zrobienia i przykrego w skutkach błędu.
public class Test extends Thread {
    public static void main(String[] args) {
        new Test().start();
        try {
            Thread.sleep(5 * 1000);
        } catch (Exception ignored) {}
        new Test().start();
    }

    public void run() {
        fakeSecurity();
    }

    public synchronized void fakeSecurity() {
        System.out.println("I'm secure: " + this);
        try {
            Thread.sleep(20 * 1000);
        } catch (Exception ignored) {}
        System.out.println("Still secure: " + this);
    }
}

Co do synchronizacji to polecam przeczytać [1] aby dowiedzieć się jak działa synchronized a następnie [2] aby dowiedzieć się dlaczego nie należy tego używać i czego należy używać w zamian (w szczególności w Javie >= 1.5).

Co do wątków:
1) Blokująca funkcja oddaje sterowanie do JVM (Thread.sleep() też jest blokującą funkcją).
2) Wątki wcale nie muszą mieć przydzielonego tego samego czasu - wystarczy że jeden z nich częściej blokuje.
3) Nawet jeśli wydaje Ci się, że wątek powinien pracować cały czas w pętli to wrzuć tam sleep() - nie ma po co smażyć procesora.

[1] http://today.java.net/pub/a/today/2004/08/02/sync1.html
[2] http://today.java.net/pub/a/today/2004/09/15/sync2.html

Offline Radarek

  • Użytkownik

# Kwiecień 13, 2007, 10:47:01
Cytuj
No i jeszcze jedno zagadnienie:
Załóżmy że mój server składa się...

Jeśli wątek nasłuchujący zostanie zablokowany i czeka na połączenie to nie zużywa czasu procesora (zostanie obudzony dopiero wtedy jak zostanie nawiązane jakieś połączenie), także Twoje obawy są niepotrzebne :).

Cytuj
Co do synchronizacji to polecam przeczytać [1] aby dowiedzieć się jak działa synchronized a następnie [2] aby dowiedzieć się dlaczego nie należy tego używać i czego należy używać w zamian (w szczególności w Javie >= 1.5).
Nie bardzo rozumiem. Zerknąłem na na artykuł i jakoś nie widzę, żeby autor nakłaniał do nie używania synchronized? I co ma do tego java 1.5?
« Ostatnia zmiana: Kwiecień 13, 2007, 11:40:03 wysłana przez Radarek »

Offline Pirs01

  • Użytkownik

# Kwiecień 13, 2007, 14:08:29
Cytuj
Pirs01: zwróć uwagę na powyższe, bo zapominając o takim zachowaniu można często narobić sobie kłopotów (trzeba pamiętać, że to co jest w synchronized() to obiekt blokady a synchronizacja będzie dotyczyła tylko tych wątków w których obiekt blokady jest ten sam).
Masz rację, poprostu miałem błędne pojęcie o tym czym jest blokada i sądziłem że dwie różne metody tego samego obiektu nie mogą być synchronizowane.
Cytuj
A nie prościej przestawić gniazdo nasłuchujące w tryb nieblokujący? Ja tak robiłem u siebie i wszystko śmiga aż miło.
Zgadza się można wszystkie gniazda ustawić w tryb nieblokujacy ale mimo to kod zatrzyma się na wywołaniu metody select() o ile metoda ta nie ma jeszcze "z czego wybierać" czyli np kiedy server czeka na nadejście pierwszego połączenia. No chyba że to znowu efekt mojej nie pełnej wiedzy. W każdym razie u mnie wiesza się zawsze na tej linijce:
while (acceptKey.selector().select() > 0 )
{
O ile acceptKey został utworzony tylko z flagą SelectionKey.OP_ACCEPT. Wątek odblokuje się i wykona resztę kodu dopiero gdy przyjdzie jakieś nowe połączenie.
Krzysiek K. tobie udało się to jakoś przeskoczyć? Bo gdyby można było nasłuchiwać nadchodzących połączeń bez wywoływania metod blokujących to by znacznie ułatwiło mi pracę.
« Ostatnia zmiana: Kwiecień 13, 2007, 14:42:13 wysłana przez Pirs01 »

bies

  • Gość
# Kwiecień 13, 2007, 17:39:13
Cytuj
Co do synchronizacji to polecam przeczytać [1] aby dowiedzieć się jak działa synchronized a następnie [2] aby dowiedzieć się dlaczego nie należy tego używać i czego należy używać w zamian (w szczególności w Javie >= 1.5).
Nie bardzo rozumiem. Zerknąłem na na artykuł i jakoś nie widzę, żeby autor nakłaniał do nie używania synchronized? I co ma do tego java 1.5?
Zobacz na sekcje o volatile. A Java 1.5 ma do tego tyle, że zawiera pakiet java.util.concurrent.

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

# Kwiecień 13, 2007, 17:44:32
Cytuj
Zgadza się można wszystkie gniazda ustawić w tryb nieblokujacy ale mimo to kod zatrzyma się na wywołaniu metody select() o ile metoda ta nie ma jeszcze "z czego wybierać" czyli np kiedy server czeka na nadejście pierwszego połączenia.
W select() można podać ile czasu maksymalnie ma czekać, w szczególności może to być zero, co spowoduje że select() jedynie sprawdzi aktualny stan gniazd i się nie zablokuje. :)

Offline parmezan

  • Użytkownik

# Kwiecień 13, 2007, 17:52:01
W select() można podać ile czasu maksymalnie ma czekać, w szczególności może to być zero, co spowoduje że select() jedynie sprawdzi aktualny stan gniazd i się nie zablokuje. :)
Nie do końca, ale blisko ;-).
Selector.select(long timeout)
Cytuj
timeout - If positive, block for up to timeout  milliseconds, more or less, while waiting for a channel to become ready; if zero, block indefinitely; must not be negative
ale Selector.selectNow() jest tym, czego potrzebujesz.
Cytuj
This method performs a non-blocking selection operation. If no channels have become selectable since the previous selection operation then this method immediately returns zero.

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

# Kwiecień 13, 2007, 18:30:36
Nie do końca, ale blisko ;-).
Heh, nie zauważyłem, ze wątek jest w dziale javowym i mówiłem o select() z C/C++. :)

Offline Pirs01

  • Użytkownik

# Kwiecień 14, 2007, 21:23:21
Jeszcze taka kwestia:

Potrzebuję aby mój server kojarzył z danym klientem różne informacje, nie tylko sam kanał (ServerSocketChannel / SocketChannel) stanowiący połączenie między nimi więc piszę klasę:
class InfoOKliencie
{
   SocketChannel polaczenie;
   Info jakiesDodatkoweInfo;
   //itd...
}
I dopuki operuję na InfoOKliencie.polaczenie wszystko jest wporządku. Ale kiedy mój kanał zarejestruję w selektorze (Selector)  to potem po wywołaniu Selector.select() nie mogę (lub nie umiem) już wybranego kanału skojarzyć z konkretnym obiektem typu InfoOKliencie. Nie bardzo wiem jak sobie z tym poradzić. Jedyne co mi przyszło do głowy to na potrzebę każdego klienta tworzyć nowy selektor i rejestrować w nim tylko jeden kanał i wtedy w klasie InfoOKliencie było by:
Selector polaczenie;zamiast:
SocketChannel polaczenie;Ale jak wiadomo takie rozwiązanie jest sprzeczne z koncepcją selektora i pod kątek wydajności też nie jest to rozsądne. Ma ktoś pomysł jak sobie z tym poradzić?
« Ostatnia zmiana: Kwiecień 14, 2007, 21:42:17 wysłana przez Pirs01 »

Offline Reg

  • Administrator
    • Adam Sawicki - Home Page

# Kwiecień 15, 2007, 11:47:55
Jeśli dobrze rozumiem że chodzi o potrzebę stworzenia tablicy gniazd podawanej do funkcji select() lub podobnej, to ja nie widzę tutaj innego wyjścia niż tworzenie osobnej tablicy takich gniazd, obok kolekcji obiektów klasy InfoOKliencie. Taka tablica może być tworzona w każdej "klatce" od nowa lub utrzymywana na stałe i modyfikowana w razie potrzeby.