Planowanie żądań
Aktywacje ziarna mają model wykonywania jednowątkowego i domyślnie przetwarzają każde żądanie od początku do ukończenia, zanim następne żądanie będzie mogło rozpocząć przetwarzanie. W niektórych okolicznościach może być pożądane, aby aktywacja przetwarzała inne żądania, podczas gdy jedno żądanie oczekuje na ukończenie operacji asynchronicznej. Z tego i innych powodów Orleans deweloper może kontrolować zachowanie przeplatania żądań zgodnie z opisem w sekcji Reentrancy . Poniżej przedstawiono przykład planowania żądań nieobsługijących, co jest zachowaniem domyślnym w programie Orleans.
Rozważmy następującą PingGrain
definicję:
public interface IPingGrain : IGrainWithStringKey
{
Task Ping();
Task CallOther(IPingGrain other);
}
public class PingGrain : Grain, IPingGrain
{
private readonly ILogger<PingGrain> _logger;
public PingGrain(ILogger<PingGrain> logger) => _logger = logger;
public Task Ping() => Task.CompletedTask;
public async Task CallOther(IPingGrain other)
{
_logger.LogInformation("1");
await other.Ping();
_logger.LogInformation("2");
}
}
W naszym przykładzie biorą udział dwa ziarna typu PingGrain
A i B. Obiekt wywołujący wywołuje następujące wywołanie:
var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");
await a.CallOther(b);
Przepływ wykonywania jest następujący:
- Wywołanie dotrze do A, które rejestruje
"1"
, a następnie wykonuje wywołanie do B. - Funkcja B zwraca natychmiast z
Ping()
powrotem do A. - Dzienniki
"2"
i powrót do oryginalnego elementu wywołującego.
Podczas gdy A oczekuje na wywołanie do B, nie może przetworzyć żadnych żądań przychodzących. W rezultacie, jeśli A i B miały zadzwonić do siebie jednocześnie, mogą zakleszczeć podczas oczekiwania na ukończenie tych połączeń. Oto przykład na podstawie klienta wykonującego następujące wywołanie:
var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");
// A calls B at the same time as B calls A.
// This might deadlock, depending on the non-deterministic timing of events.
await Task.WhenAll(a.CallOther(b), b.CallOther(a));
Przypadek 1: wywołania nie zakleszczą się
W tym przykładzie:
- Połączenie
Ping()
z A dociera do B przed przybyciemCallOther(a)
połączenia do B. - W związku z tym usługa B przetwarza wywołanie
Ping()
przed wywołaniemCallOther(a)
. - Ponieważ usługa B przetwarza wywołanie
Ping()
, funkcja A może wrócić do elementu wywołującego. - Gdy usługa B wysyła
Ping()
wywołanie do A, A jest nadal zajęta rejestrowaniem komunikatu ("2"
), więc połączenie musi czekać przez krótki czas, ale wkrótce będzie można go przetworzyć. - Element przetwarza wywołanie
Ping()
i wraca do B, co powraca do oryginalnego elementu wywołującego.
Rozważmy mniej szczęścia serii zdarzeń: jeden, w którym ten sam kod powoduje zakleszczenie z powodu nieco innego czasu.
Przypadek 2: impas wywołań
W tym przykładzie:
- Połączenia
CallOther
docierają do odpowiednich ziarna i są przetwarzane jednocześnie. - Dziennik ziarna
"1"
i przejdź do .await other.Ping()
- Ponieważ oba ziarna są nadal zajęte (przetwarzanie
CallOther
żądania, które jeszcze nie zostało zakończone),Ping()
żądania oczekują - Po pewnym czasie określa, Orleans że upłynął limit czasu wywołania, a każde
Ping()
wywołanie powoduje zgłoszenie wyjątku. - Treść
CallOther
metody nie obsługuje wyjątku i bąbelki do oryginalnego elementu wywołującego.
W poniższej sekcji opisano, jak zapobiec zakleszczeniom, zezwalając wielu żądaniom na przeplatanie ich wykonywania ze sobą.
Ponowne wejścia
Orleans domyślnie wybiera bezpieczny przepływ wykonywania: taki, w którym stan wewnętrzny ziarna nie jest modyfikowany współbieżnie podczas wielu żądań. Współbieżne modyfikacje stanu wewnętrznego komplikują logikę i obciążają dewelopera większym obciążeniem. Ta ochrona przed tego rodzaju usterkami współbieżności ma koszt, który został wcześniej omówiony, przede wszystkim liveness: niektóre wzorce wywołań mogą prowadzić do zakleszczenia. Jednym ze sposobów uniknięcia zakleszczeń jest zapewnienie, że wywołania ziarna nigdy nie powodują cyklu. Często trudno jest napisać kod, który jest wolny od cyklu i nie może zakleszczeć. Oczekiwanie na uruchomienie każdego żądania od początku do ukończenia przed przetworzeniem następnego żądania może również zaszkodzić wydajności. Jeśli na przykład metoda ziarna wykonuje asynchroniczne żądanie do usługi bazy danych, ziarno wstrzymuje wykonywanie żądań, aż odpowiedź z bazy danych dotrze do ziarna.
Każdy z tych przypadków jest omówiony w kolejnych sekcjach. Z tych powodów Orleans udostępnia deweloperom opcje umożliwiające równoczesne wykonywanie niektórych lub wszystkich żądań, przeplatając ich wykonywanie ze sobą. W Orleanssystemie takie obawy są określane jako reentrancy lub przeplatania. Wykonując żądania jednocześnie, ziarna wykonujące operacje asynchroniczne mogą przetwarzać więcej żądań w krótszym okresie.
W następujących przypadkach może zostać przeplatonych wiele żądań:
- Klasa ziarna jest oznaczona znakiem ReentrantAttribute.
- Metoda interfejsu jest oznaczona znakiem AlwaysInterleaveAttribute.
- Predykat ziarna MayInterleaveAttribute zwraca wartość
true
.
Po ponownym uruchomieniu następujący przypadek staje się prawidłowym wykonaniem i możliwość usunięcia powyższego zakleszczenia.
Przypadek 3: ziarno lub metoda jest reentrant
W tym przykładzie ziarna A i B mogą wywoływać się jednocześnie bez możliwości planowania zakleszczenia żądań, ponieważ oba ziarna są ponownie uczestnikami. Poniższe sekcje zawierają więcej szczegółów na temat ponownej rejestracji.
Ziarna ponownego uczestnika
Klasy Grain implementacji mogą być oznaczone znakiem ReentrantAttribute , aby wskazać, że różne żądania mogą być swobodnie przeplatane.
Innymi słowy, ponowna aktywacja może rozpocząć wykonywanie kolejnego żądania, podczas gdy poprzednie żądanie nie zakończyło przetwarzania. Wykonywanie jest nadal ograniczone do pojedynczego wątku, więc aktywacja nadal wykonuje jeden obrót jednocześnie, a każdy obrót jest wykonywany w imieniu tylko jednego z żądań aktywacji.
Kod ziarna ponownego uczestnika nigdy nie uruchamia wielu fragmentów kodu ziarna równolegle (wykonywanie kodu ziarna jest zawsze jednowątkowy), ale ponowne uczestnicy ziarna mogą zobaczyć wykonywanie kodu dla różnych żądań przeplatania. Oznacza to, że kontynuacja zmienia się z różnych żądań może przeplatać.
Na przykład, jak pokazano w poniższym pseudokodzie, należy wziąć pod uwagę, że Foo
i Bar
są dwiema metodami tej samej klasy ziarna:
Task Foo()
{
await task1; // line 1
return Do2(); // line 2
}
Task Bar()
{
await task2; // line 3
return Do2(); // line 4
}
Jeśli to ziarno jest oznaczone ReentrantAttribute, wykonanie Foo
i Bar
może przeplatać.
Na przykład możliwa jest następująca kolejność wykonywania:
Wiersz 1, wiersz 3, wiersz 2 i wiersz 4. Oznacza to, że przeplata się z różnych żądań.
Jeśli ziarno nie było ponownie uczestnikiem, jedynymi możliwymi wykonaniami będą: wiersz 1, wiersz 2, wiersz 3, wiersz 4 LUB: wiersz 3, wiersz 4, wiersz 1, wiersz 2 (nowe żądanie nie może rozpocząć się przed zakończeniem poprzedniego).
Głównym kompromisem w wyborze między ziarnami reentrant i nonreentrant jest złożoność kodu, dzięki czemu przeplatanie działa poprawnie, a trudnością z jego przyczyną.
W trywialnym przypadku, gdy ziarna są bezstanowe, a logika jest prosta, mniej (ale nie za mało, tak aby wszystkie wątki sprzętowe były używane) ziarna ponownego uczestnika powinny być ogólnie nieco bardziej wydajne.
Jeśli kod jest bardziej złożony, wówczas większa liczba ziarna niepochodzących, nawet jeśli jest nieco mniej wydajna, powinna zaoszczędzić dużo smutku w rozwiązywaniu nieobiektywnych problemów przeplatania.
W końcu odpowiedź zależy od specyfiki aplikacji.
Metody przeplatania
Metody interfejsu ziarna oznaczone , AlwaysInterleaveAttributezawsze przeplata wszelkie inne żądania i zawsze mogą być przeplatane z dowolnym innym żądaniem, nawet żądania dla metod innych niż[AlwaysInterleave].
Rozważmy następujący przykład:
public interface ISlowpokeGrain : IGrainWithIntegerKey
{
Task GoSlow();
[AlwaysInterleave]
Task GoFast();
}
public class SlowpokeGrain : Grain, ISlowpokeGrain
{
public async Task GoSlow()
{
await Task.Delay(TimeSpan.FromSeconds(10));
}
public async Task GoFast()
{
await Task.Delay(TimeSpan.FromSeconds(10));
}
}
Rozważ przepływ wywołań zainicjowany przez następujące żądanie klienta:
var slowpoke = client.GetGrain<ISlowpokeGrain>(0);
// A. This will take around 20 seconds.
await Task.WhenAll(slowpoke.GoSlow(), slowpoke.GoSlow());
// B. This will take around 10 seconds.
await Task.WhenAll(slowpoke.GoFast(), slowpoke.GoFast(), slowpoke.GoFast());
Wywołania nie GoSlow
są przeplatane, więc łączny czas wykonywania dwóch GoSlow
wywołań trwa około 20 sekund. Z drugiej strony GoFast
jest oznaczony jako AlwaysInterleaveAttribute, a trzy wywołania są wykonywane współbieżnie, kończąc w sumie około 10 sekund zamiast wymagać co najmniej 30 sekund do ukończenia.
Metody readonly
Jeśli metoda ziarna nie modyfikuje stanu ziarna, można bezpiecznie wykonywać jednocześnie z innymi żądaniami. Wskazuje ReadOnlyAttribute , że metoda nie modyfikuje stanu ziarna. Metody oznaczania jako ReadOnly
umożliwiają Orleans przetwarzanie żądań jednocześnie z innymi ReadOnly
żądaniami, co może znacznie poprawić wydajność aplikacji. Rozważmy następujący przykład:
public interface IMyGrain : IGrainWithIntegerKey
{
Task<int> IncrementCount(int incrementBy);
[ReadOnly]
Task<int> GetCount();
}
Ponowne wywołania łańcucha wywołań
Jeśli ziarno wywołuje metodę, która na innym ziarnie, które następnie wywołuje z powrotem do oryginalnego ziarna, wywołanie spowoduje zakleszczenie, chyba że wywołanie jest ponowne. Ponowne wywołania można włączyć dla poszczególnych lokacji za pomocą ponownego wywołania łańcucha wywołań. Aby włączyć ponowne wywołanie łańcucha wywołań, wywołaj AllowCallChainReentrancy() metodę, która zwraca wartość, która umożliwia ponowne wywołowanie z dowolnego obiektu wywołującego dalej w dół łańcucha wywołań do momentu usunięcia. Obejmuje to ponowne wyjęcie z ziarna wywołującego samą metodę. Rozważmy następujący przykład:
public interface IChatRoomGrain : IGrainWithStringKey
{
ValueTask OnJoinRoom(IUserGrain user);
}
public interface IUserGrain : IGrainWithStringKey
{
ValueTask JoinRoom(string roomName);
ValueTask<string> GetDisplayName();
}
public class ChatRoomGrain : Grain<List<(string DisplayName, IUserGrain User)>>, IChatRoomGrain
{
public async ValueTask OnJoinRoom(IUserGrain user)
{
var displayName = await user.GetDisplayName();
State.Add((displayName, user));
await WriteStateAsync();
}
}
public class UserGrain : Grain, IUserGrain
{
public ValueTask<string> GetDisplayName() => new(this.GetPrimaryKeyString());
public async ValueTask JoinRoom(string roomName)
{
// This prevents the call below from triggering a deadlock.
using var scope = RequestContext.AllowCallChainReentrancy();
var roomGrain = GrainFactory.GetGrain<IChatRoomGrain>(roomName);
await roomGrain.OnJoinRoom(this.AsReference<IUserGrain>());
}
}
W poprzednim przykładzie UserGrain.JoinRoom(roomName)
wywołuje metodę , ChatRoomGrain.OnJoinRoom(user)
która próbuje wywołać element , UserGrain.GetDisplayName()
aby uzyskać nazwę wyświetlaną użytkownika. Ponieważ ten łańcuch wywołań obejmuje cykl, spowoduje to zakleszczenie, jeśli UserGrain
nie zezwala na ponowne wprowadzenie przy użyciu żadnego z obsługiwanych mechanizmów omówionych w tym artykule. W tym przypadku używamy metody AllowCallChainReentrancy(), która umożliwia tylko roomGrain
wywołanie z powrotem do obiektu UserGrain
. Dzięki temu możesz uzyskać szczegółową kontrolę nad tym, gdzie i jak jest włączona ponowna zmiana.
Gdyby zamiast tego zapobiec impasowi poprzez dodanie adnotacji GetDisplayName()
do deklaracji metody przy [AlwaysInterleave]
IUserGrain
użyciu polecenia , można zezwolić na przeplatanie GetDisplayName
wywołania z dowolną inną metodą. Zamiast tego zezwalasz tylko roomGrain
na wywoływanie metod na naszym ziarnie i tylko do momentu scope
usunięcia.
Pomijanie ponownego wywołania łańcucha wywołań
Ponowne wywołania łańcucha wywołań można również pominąć przy użyciu SuppressCallChainReentrancy() metody . Ma to ograniczoną użyteczność dla deweloperów końcowych, ale jest ważne w przypadku użytku wewnętrznego przez biblioteki, które rozszerzają Orleans funkcje ziarna, takie jak kanały przesyłania strumieniowego i emisji, aby zapewnić deweloperom pełną kontrolę nad włączeniem ponownego uruchamiania łańcucha wywołań.
Metoda GetCount
nie modyfikuje stanu ziarna, dlatego jest oznaczona znakiem ReadOnly
. Wywołania oczekujące na wywołanie tej metody nie są blokowane przez inne ReadOnly
żądania do ziarna, a metoda zwraca natychmiast.
Ponowne stosowanie predykatu
Klasy ziarna mogą określać predykat w celu określenia przeplatania na podstawie wywołania, sprawdzając żądanie. Atrybut [MayInterleave(string methodName)]
zapewnia tę funkcję. Argumentem atrybutu jest nazwa metody statycznej w klasie ziarna, która akceptuje InvokeMethodRequest obiekt i zwraca wartość wskazującą bool
, czy żądanie powinno być przeplatane.
Oto przykład, który umożliwia przeplatanie, jeśli typ argumentu [Interleave]
żądania ma atrybut:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class InterleaveAttribute : Attribute { }
// Specify the may-interleave predicate.
[MayInterleave(nameof(ArgHasInterleaveAttribute))]
public class MyGrain : Grain, IMyGrain
{
public static bool ArgHasInterleaveAttribute(IInvokable req)
{
// Returning true indicates that this call should be interleaved with other calls.
// Returning false indicates the opposite.
return req.Arguments.Length == 1
&& req.Arguments[0]?.GetType()
.GetCustomAttribute<InterleaveAttribute>() != null;
}
public Task Process(object payload)
{
// Process the object.
}
}