Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Aktywacje ziarna mają jednowątkowy model wykonywania. Domyślnie przetwarzają każde żądanie od początku do ukończenia, zanim następne żądanie może rozpocząć przetwarzanie. W niektórych okolicznościach może być pożądane, aby aktywacja przetwarzała inne żądania, podczas gdy jedno żądanie czeka na ukończenie operacji asynchronicznej. Z tego i innych powodów Orleans pozwala na pewną kontrolę nad zachowaniem przeplatania żądań, jak opisano w sekcji Reentrancy. Poniżej przedstawiono przykład planowania żądań nie-reentrantnych, co jest zachowaniem domyślnym w 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 inicjuje 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. -
B natychmiast wraca z
Ping()
do A. -
Loguje
"2"
i wraca do oryginalnego wywołującego.
Podczas gdy A oczekuje na wywołanie B, nie może przetworzyć żadnych żądań przychodzących. W rezultacie, jeśli A i B miałyby zadzwonić do siebie jednocześnie, mogą utknąć w martwym punkcie 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 się nie zablokują
W tym przykładzie:
- Połączenie
Ping()
z A dociera do B przed przyjściem połączeniaCallOther(a)
do B. - W związku z tym usługa B przetwarza wywołanie
Ping()
przed wywołaniemCallOther(a)
. - Ponieważ B przetwarza wywołanie
Ping()
, A może wrócić do elementu wywołującego. - Gdy B wysyła
Ping()
wywołanie do A, A jest nadal zajęta rejestrowaniem swojego komunikatu ("2"
), więc wywołanie musi poczekać przez krótki czas, ale wkrótce może zostać przetworzone. -
A przetwarza wywołanie
Ping()
i wraca do B, który wraca do oryginalnego wywołującego.
Rozważmy mniej fortunny ciąg zdarzeń, w którym ten sam kod powoduje zakleszczenie z powodu nieco innego czasowania.
Przypadek 2: Impas wywołań
W tym przykładzie:
- Połączenia
CallOther
docierają do odpowiednich ziaren i są przetwarzane jednocześnie. - Dziennik ziarna
"1"
i przejdź do .await other.Ping()
- Obie ziarna są nadal zajęte, ponieważ przetwarzają
CallOther
żądanie, które jeszcze nie zostało zakończone, więcPing()
żądania muszą czekać. - 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 przechodzi do oryginalnego miejsca wywołania.
W poniższej sekcji opisano, jak zapobiec blokadom, zezwalając wielu żądaniom na przeplatanie się ich wykonywania.
Ponowne wejścia
Orleans domyślnie jest to bezpieczny przepływ wykonywania, w którym stan wewnętrzny ziarna nie jest modyfikowany jednocześnie przez wiele żądań. Współbieżne modyfikacje komplikują logikę i nakładają większe obciążenie na Ciebie, dewelopera. Ochrona przed usterkami współbieżności wiąże się z kosztami, przede wszystkim żywotność: niektóre wzorce wywołań mogą prowadzić do zakleszczenia, o czym wspomniano wcześniej. Jednym ze sposobów uniknięcia zakleszczeń jest zapewnienie, że wywołania ziarna nigdy nie tworzą cyklu. Często trudno jest napisać kod, który jest wolny od zablokowań i nie powoduje zakleszczenia. Oczekiwanie na uruchomienie każdego żądania od początku do ukończenia przed przetworzeniem następnego 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 żądania do momentu otrzymania odpowiedzi od bazy danych.
Każdy z tych przypadków jest omówiony w poniższych sekcjach. Z tych powodów Orleans udostępnia opcje zezwalania niektórym lub wszystkim żądaniom na wykonywanie współbieżnie, przeplatając ich wykonywanie. W Orleans odnosimy się do takich obaw, jak reentrancy lub interleaving. 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
.
W przypadku ponownego wywołania następujący przypadek staje się prawidłowym wykonaniem, co pozwala usunąć możliwość zakleszczenia opisanego powyżej.
Przypadek 3. Ziarno lub metoda jest reentrantna
W tym przykładzie ziarna A i B mogą wywoływać siebie jednocześnie bez potencjalnych zakleszczeń planowania żądań, ponieważ oba ziarna są reentrantne. Poniższe sekcje zawierają więcej szczegółów na temat ponownej rejestracji.
Ziarna wklęsłe
Klasy implementacji można oznaczać GrainReentrantAttribute, aby wskazać, że różne żądania mogą być swobodnie przeplatane.
Innymi słowy, aktywacja reentrant może rozpocząć przetwarzanie innego żądania, podczas gdy poprzednie żądanie nie zostało zakończone. Wykonanie jest nadal ograniczone do pojedynczego wątku, więc aktywacja wykonuje jeden obrót naraz, a każdy obrót jest wykonywany w imieniu tylko jednego z żądań aktywacji.
Kod ziarna reentrantnego nigdy nie uruchamia wielu fragmentów kodu ziarna równolegle (wykonywanie zawsze odbywa się jednowątkowo), ale ziarna reentrantne mogą doświadczać przeplatania się wykonywania kodu dla różnych żądań. Oznacza to, że kontynuacje różnych żądań mogą się 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 ten wątek jest oznaczony ReentrantAttribute, wykonanie Foo
i Bar
może się przeplatać.
Na przykład możliwa jest następująca kolejność wykonywania:
Wiersz 1, wiersz 3, wiersz 2 i wiersz 4. Oznacza to, że kolejki z różnych żądań przeplatają się.
Gdyby ziarno nie było ponownie wchodzące, jedynymi możliwymi sposobami wykonania byłoby: 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 przy wyborze między ziarnami reentrantnymi a niereentrantnymi jest złożoność kodu wymagana do prawidłowego działania przeplatania oraz trudności w rozumowaniu na ten temat.
W trywialnym przypadku, gdy ziarna są bezstanowe, a logika jest prosta, stosowanie mniejszej liczby reentrancyjnych ziaren (ale nie zbyt małej, aby zapewnić wykorzystanie wszystkich wątków sprzętowych) powinno być ogólnie nieco bardziej wydajne.
Jeśli kod jest bardziej złożony, użycie większej liczby niewielokrotnego wejścia, nawet jeśli jest nieco mniej wydajnie, może oszczędzić wielu problemów podczas debugowania trudnych do zidentyfikowania problemów z przeplataniem.
W końcu odpowiedź zależy od specyfiki aplikacji.
Metody przeplatania
Metody interfejsu ziarna oznaczone AlwaysInterleaveAttribute zawsze przeplatają się z wszelkimi innymi żądaniami i zawsze mogą być przeplatane przez dowolne inne żądania, nawet dotyczące metod innych [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 do GoSlow
nie są przeplatane, więc łączny czas wykonywania dwóch wywołań GoSlow
wynosi około 20 sekund. Z drugiej strony GoFast
jest oznaczony jako AlwaysInterleaveAttribute. Trzy wywołania są wykonywane jednocześnie, co pozwala na ich zakończenie w około 10 sekund zamiast wymagania co najmniej 30 sekund.
Metody tylko do odczytu
Jeśli metoda ziarna nie modyfikuje jego stanu, można ją bezpiecznie wykonywać równocześ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 zwiększyć wydajność aplikacji. Rozważmy następujący przykład:
public interface IMyGrain : IGrainWithIntegerKey
{
Task<int> IncrementCount(int incrementBy);
[ReadOnly]
Task<int> GetCount();
}
Metoda GetCount
nie modyfikuje stanu ziarna, dlatego jest oznaczona jako ReadOnly
. Wywołania oczekujące na wywołanie tej metody nie są blokowane przez inne ReadOnly
żądania kierowane do grainu, a metoda zwraca wartość natychmiast.
Reentrancja łańcucha wywołań
Jeśli ziarno wywołuje metodę na innym ziarnie, które następnie wywołuje z powrotem do ziarna oryginalnego, wywołanie powoduje zakleszczenie, chyba że wywołanie jest reentrantne. Można włączyć ponowne wywołanie dla poszczególnych lokacji przy użyciu ponownego wywołania łańcucha wywołań. Aby włączyć ponowne wywołania łańcucha wywołań, wywołaj metodę AllowCallChainReentrancy() . Ta metoda zwraca wartość umożliwiającą ponowne wejście z dowolnego wywołującego dalej w dół łańcucha wywołań, aż do usunięcia wartości. Obejmuje to ponowne wejście do ziarna, które samo wywołuje 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 ChatRoomGrain.OnJoinRoom(user)
, która próbuje wywołać UserGrain.GetDisplayName()
aby uzyskać nazwę wyświetlaną użytkownika. Ponieważ ten łańcuch wywołań obejmuje cykl, powoduje zakleszczenie, jeśli UserGrain
nie zezwala na ponowne wejście przy użyciu jednego z obsługiwanych mechanizmów omówionych w tym artykule. W tym przypadku używamy AllowCallChainReentrancy(), który pozwala tylko roomGrain
na przywołanie do UserGrain
. Zapewnia to szczegółową kontrolę nad tym, gdzie i jak jest włączona ponowna reentrancyjność.
Gdyby zamiast tego zapobiec zakleszczeniu przez adnotowanie deklaracji metody GetDisplayName()
na IUserGrain
za pomocą [AlwaysInterleave]
, umożliwiłoby to każdemu zbożu przeplatanie wywołania GetDisplayName
z dowolną inną metodą. Korzystając z AllowCallChainReentrancy
, można wywoływać tylkoroomGrain
metody na UserGrain
, i to tylko do momentu, gdy scope
zostanie zlikwidowany.
Pomijanie ponownego wywołania łańcucha wywołań
Można również zablokować ponowne wejście do łańcucha wywołań przy użyciu metody SuppressCallChainReentrancy(). Ma to ograniczoną użyteczność dla deweloperów końcowych, ale jest ważne dla użytku wewnętrznego przez biblioteki rozszerzające Orleans funkcjonalność ziarna, takie jak przesyłanie strumieniowe i kanały emisji, aby deweloperzy mogli mieć pełną kontrolę nad włączeniem reentrantności łańcucha wywołań.
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. Ta metoda przyjmuje obiekt InvokeMethodRequest i zwraca bool
, wskazując, czy żądanie powinno zostać przeplatane.
Oto przykład, który umożliwia przeplatanie, jeśli typ argumentu żądania ma atrybut [Interleave]
:
[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.
}
}