2011-01-05 27 views
29

Modeluję grę, w której porusza się wielu graczy (wątków) w tym samym czasie. Informacja, gdzie znajduje się gracz w danym momencie, jest zapisana dwa razy: gracz ma zmienną "hostField", która odnosi się do pola na planszy, a każde pole ma ArrayList przechowujące graczy, którzy aktualnie znajdują się na tym polu.blok synchronizowany - blokuj więcej niż jeden obiekt

Nie jestem zbyt zadowolony z faktu, że mam nadmiarowe informacje, ale nie udało mi się tego uniknąć, nie przechodząc przez duży zbiór danych.

Jednakże, gdy gracz przesuwa się z jednego pola do drugiego, chciałbym się upewnić (1), że nadmiarowe informacje pozostają połączone (2) nikt inny nie manipuluje polem w tej chwili.

Dlatego muszę zrobić coś podobnego

synchronized(player, field) { 
    // code 
} 

co nie jest możliwe, prawda?

Co należy zrobić? :)

+3

Ponieważ operacje są bardzo krótkotrwałe, może się okazać, że użycie tylko jednego globalnego zamka działa na tyle dobrze, że nie można stwierdzić różnicy. Korzystanie z jednego zamka może ograniczać się do 200 000 ruchów na sekundę (czy to wystarczy?), Ale może uprościć kod i nie dostaniesz zakleszczenia. –

Odpowiedz

19

Dodałem kolejną odpowiedź, ponieważ nie mogę jeszcze dodawać komentarzy do postów innych osób.

  • W rzeczywistości synchronizacja dotyczy kodu, a nie obiektów lub danych. Odniesienie do obiektu używane jako parametr w zsynchronizowanym bloku reprezentuje blokadę.

więc jeśli masz kod jak:

class Player { 

    // Same instance shared for all players... Don't show how we get it now. 
    // Use one dimensional board to simplify, doesn't matter here. 
    private List<Player>[] fields = Board.getBoard(); 

    // Current position 
    private int x; 

    public synchronized int getX() { 
    return x; 
    } 

    public void setX(int x) { 
    synchronized(this) { // Same as synchronized method 
     fields[x].remove(this); 
     this.x = x; 
     field[y].add(this); 
    } 
    } 
} 

czasu pomimo beeing na zsynchronizowanym bloku dostępu do pola nie jest chroniony, ponieważ blokada nie jest taka sama (to jest w różnych przypadkach). Lista Twoich graczy na twojej planszy może stać się niespójna i spowodować wyjątki w czasie wykonywania.

Zamiast jeśli piszesz poniższy kod, to będzie działać, ponieważ mamy tylko jeden wspólny blokady dla wszystkich graczy:

class Player { 

    // Same instance shared for all players... Don't show how we get it now. 
    // Use one dimensional board to simplify, doesn't matter here. 
    private List<Player>[] fields; 

    // Current position 
    private int x; 

    private static Object sharedLock = new Object(); // Any object's instance can be used as a lock. 

    public int getX() { 
    synchronized(sharedLock) { 
     return x; 
    } 
    } 

    public void setX(int x) { 
    synchronized(sharedLock) { 
     // Because of using a single shared lock, 
     // several players can't access fields at the same time 
     // and so can't create inconsistencies on fields. 
     fields[x].remove(this); 
     this.x = x; 
     field[y].add(this); 
    } 
    } 
} 

=> Należy używać tylko jedną blokadę na dostęp do wszystkich graczy lub twoi Stan zarządu będzie niespójny.

+0

tak, użycie jednego zamka prawdopodobnie by działało (tak samo jak blokowanie całego pola lub blokowanie Player.class). Obawiam się, że nie mogę pójść tą drogą, ponieważ specyfikacja mojego ćwiczenia mówi, że nie wolno używać zbyt dużych zamków ... Próbowałem nawet małych zamków w stosunku do dużych zamków, ale okazało się, że mój program działa znacznie szybciej z małymi zamkami. Jeszcze raz dziękuję za twoją długą odpowiedź! Chciałbym wspierać Cię zdobywając punkty! :) – speendo

44

Trywialny rozwiązaniem byłoby

synchronized(player) { 
    synchronized(field) { 
     // code 
    } 
} 

jednak upewnić się, że zawsze zablokować zasoby w tym samym celu uniknięcia zakleszczenia.

Należy zauważyć, że w praktyce wąskim gardłem jest pole, więc wystarczy jedna blokada na polu (lub na dedykowanym, wspólnym obiekcie blokady, jak słusznie wskazano @ ripper234) (chyba że jednocześnie manipuluje się graczami w innych sprzeczne sposoby).

+1

Po prostu chciałem to napisać i dodać, że powinieneś używać dedykowanych obiektów blokujących i nie blokować samych obiektów biznesowych, gdy dwóch facetów z pracy wpadło i potrzebowało mojej pomocy :) Nie ma poprawy reputacji. – ripper234

+0

+1: od razu do rzeczy. Na marginesie: Można zbudować klasę z obiema polami i użyć jej jako modelu pozycji gracza. Dzięki temu może istnieć tylko jedna blokada, ale może ona w ogóle nie pasować do istniejącego modelu programowania. – Rekin

+2

@ ripper234, oh współpracownicy - zawsze zawracając sobie głowę ciekawostkami i okradając nasz cenny czas, który moglibyśmy spokojnie spędzić na SO ;-) –

1

Nie powinieneś czuć się źle z powodu swojego modelowania - jest to tylko dwukierunkowa nawigacja.

Jeśli zająć się (jak w innych udzielanych odpowiedziach) manipulowaniem atomowym, np. w metodach Field, to w porządku.

 

public class Field { 

    private Object lock = new Object(); 

    public removePlayer(Player p) { 
    synchronized (lock) { 
     players.remove(p); 
     p.setField(null); 
    } 
    } 

    public addPlayer(Player p) { 
    synchronized (lock) { 
     players.add(p); 
     p.setField(this); 
    } 
    } 
} 

 

Byłoby dobrze, gdyby "Player.setField" były chronione.

Jeśli potrzebujesz więcej atomowości dla semantyki "przenieś", przejdź o jeden poziom wyżej na planszę.

+0

Nie do końca rozumiem "prywatną blokadę obiektu = nowy obiekt();" wzór. Czytałem to wcześniej, ale tak naprawdę nie rozumiałem ... ale jeśli dobrze cię zrozumiałem, po prostu upewnij się, że obie zmiany zachodzą w tym samym czasie, prawda? Dlaczego nie zablokujesz pola, ale "zablokujesz"? – speendo

+1

@Marcel as @ ripper234 już stwierdzono - jest to dobra praktyka, ponieważ ukrywa monitor używany do blokowania dostępu publicznego. Każdy może zablokować instancję pola, tylko pole na jego prywatnej "blokadzie". W twoim przykładzie to nie robi różnicy. Po prostu użyj go jako dobrego stylu. Wzorzec ten pozwoli na przykład na zwiększenie współbieżności w bardziej skomplikowanych scenariuszach poprzez użycie większej liczby obiektów blokujących itp. – mtraut

7

W przypadku współbieżności zawsze trudno jest podać dobre odpowiedzi. To bardzo zależy od tego, co naprawdę robisz i co naprawdę ma znaczenie.

Z mojego zrozumienia, ruch gracz obejmować:

1 Updading pozycję gracza.

2 Usuwanie odtwarzacza z poprzedniego pola.

3 Dodawanie gracza do nowego pola.

Wyobraź użyć kilka zamków w tym samym czasie, ale nabyć tylko jedną na raz: - kolejny gracz może doskonale wyglądać w nieodpowiednim momencie, w zasadzie między 1 & 2 lub 2 & 3. Niektóre gracz może pojawić się disapeared z planszy na przykład.

Wyobraź sobie DO imbricked blokowania tak:

synchronized(player) { 
    synchronized(previousField) { 
    synchronized(nextField) { 
     ... 
    } 
    } 
} 

Problemem jest ... To nie działa, zobacz ten kolejność wykonywania przez 2 wątków:

Thread1 : 
Lock player1 
Lock previousField 
Thread2 : 
Lock nextField and see that player1 is not in nextField. 
Try to lock previousField and so way for Thread1 to release it. 
Thread1 : 
Lock nextField 
Remove player1 from previous field and add it to next field. 
Release all locks 
Thread 2 : 
Aquire Lock on previous field and read it : 

wątku 2 myślę, że gracz 1 został usunięty z całej planszy. Jeśli jest to problem związany z aplikacją, nie można użyć tego rozwiązania.

Dodatkowy problem z zablokowaniem blokującym: wątki mogą zostać zablokowane. Wyobraź 2 graczy: wymieniają swoją pozycję dokładnie w tym samym czasie:

player1 aquire it's own position at the same time 
player2 aquire it's own position at the same time 
player1 try to acquire player2 position : wait for lock on player2 position. 
player2 try to acquire player1 position : wait for lock on player1 position. 

=> Obaj gracze są zablokowane.

Najlepszym rozwiązaniem moim zdaniem jest użycie tylko jednego zamka, na cały stan gry.

Gdy gracz chce odczytać stan, blokuje cały stan gry (płyta &) i tworzy kopię dla własnego użytku. Może następnie przetwarzać bez blokady.

Gdy gracz chce zapisać stan, blokuje cały stan gry, zapisuje nowy stan, a następnie zwalnia blokadę.

=> Blokada jest ograniczona do operacji odczytu/zapisu stanu gry. Gracz może wykonać "długie" badanie stanu deski na swojej własnej kopii.

Zapobiega to jakiemukolwiek nieodpornemu stanowi, takiemu jak gracz na kilku polach lub nie ma go wcale, ale nie przeszkadza graczowi w używaniu "starego" stanu.

Może wydawać się dziwne, ale jest to typowy przypadek gry w szachy. Kiedy czekasz, aż inny gracz się ruszy, zobaczysz planszę jak przed ruchem. Nie wiesz, jaki ruch wykona drugi gracz i dopóki on się nie poruszy, będziesz pracował nad "starym" stanem.

+0

wow! Zrobiłeś wielki wysiłek, odpowiadając na moje pytanie! Dziękuję Ci bardzo!!! – speendo

+0

o zakleszczeniu player1 i player2 z powodu przejęcia własnych współrzędnych, nie używają synchronizacji i nie powodują zniekształceń współrzędnych, powinny zapobiegać zakleszczeniom, aby następujące czynności mogły się bezpiecznie zdarzyć: '> gracz1 próbuje uzyskać pozycję gracza2; > player2 spróbuj zdobyć pozycję gracza 1; "W pewnym sensie, volatile tworzy wyimaginowany" wspólny zamek ", w którym wiele wątków może" zablokować "współrzędne, ponieważ wartość jest zawsze aktualizowana w stosie danego wątku. –

+0

Wiem, że to stary q/a, ale twój model wygląda jak bzdura. Wątek1 zablokował previousField. Thread2 zablokował nextFIeld i czeka na blokadę dla previousField aktualnie posiadanego przez Thread1. Nie ma mowy, aby Thread1 minął "Lock nextField", aby "usunąć gracza1". To, co tu masz, to dwa zakleszczone wątki, a nie "brakujący Gracz1 z planszy", jak sugerujesz. Gracz 1 nie może być w ogóle usunięty, ponieważ zakleszczenie ma miejsce przed usunięciem. – Grod

0

Czytając wszystkie swoje odpowiedzi, próbowałem zastosować następującą konstrukcję:

  1. graczy tylko zamek, a nie pola
  2. Do prac polowych wyłącznie w zsynchronizowanych metod/bloków
  3. w zsynchronizowany metody/blok zawsze najpierw sprawdź, czy warunki wstępne, które spowodowały wywołanie zsynchronizowanej metody/bloku, nadal istnieją:

Myślę, że 1. unika zakleszczeń i 3. jest ważny jako rzecz s może się zmienić, gdy gracz czeka.

Co więcej, mogę przejść bez blokowania pól, ponieważ w mojej grze więcej niż jeden gracz może pozostać na polu, tylko w przypadku niektórych wątków interakcja musi zostać wykonana. Ta interakcja może być wykonana przez synchronizację odtwarzaczy - nie ma potrzeby synchronizowania pól ...

Co myślisz?

Powiązane problemy