Udostępnij za pośrednictwem


Zadania zewnętrzne i ziarna

Zgodnie z projektem wszystkie podzadania pochodzące z kodu ziarna (np. przy użyciu await, ContinueWith lub Task.Factory.StartNew) przekazują do tej samej aktywacji TaskScheduler co zadanie nadrzędne. W związku z tym dziedziczą ten sam model wykonywania jednowątkowego co pozostała część kodu ziarna. Jest to główny punkt związany z wykonywaniem jednowątkowego współbieżności opartej na ziarnie.

W niektórych przypadkach kod ziarna może wymagać "wyrwania się" z Orleans modelu planowania zadań i "zrobienia czegoś specjalnego", takiego jak wyraźne wskazanie Task innego harmonogramu zadań lub platformy .NET ThreadPool. Przykładem jest sytuacja, gdy kod ziarnisty musi wykonać synchroniczne zdalne wywołanie blokujące (np. zdalne I/O). Wykonanie tego wywołania blokującego w kontekście ziarna blokuje ziarno i w ten sposób nigdy nie powinno być wykonywane. Zamiast tego kod ziarna może wykonać ten fragment kodu blokującego w wątku puli wątków, poczekać na zakończenie tego wykonania, a następnie kontynuować w kontekście ziarna. Oczekuje się, że wyjście poza Orleans harmonogram będzie bardzo zaawansowanym i rzadko wymaganym scenariuszem użycia, wykraczającym poza typowe wzorce stosowania.

Interfejsy API oparte na zadaniach

  1. await, TaskFactory.StartNew (patrz poniżej), Task.ContinueWith, Task.WhenAny, Task.WhenAll i Task.Delay są zgodne z bieżącym harmonogramem zadań. Oznacza to, że użycie ich w domyślny sposób bez przekazywania innego TaskSchedulerelementu powoduje ich wykonanie w kontekście ziarna.

  2. Zarówno Task.Run , jak i endMethod pełnomocnik TaskFactory.FromAsyncnie przestrzegają bieżącego harmonogramu zadań. Obaj używają TaskScheduler.Default harmonogramu , harmonogramu zadań puli wątków platformy .NET. W związku z tym kod wewnątrz Task.Run oraz endMethod w Task.Factory.FromAsynczawsze jest uruchamiany na puli wątków platformy .NET, poza jednowątkowym modelem wykonawczym dla ziaren Orleans. Jednak każdy kod po await Task.Run lub await Task.Factory.FromAsync jest uruchamiany ponownie pod kontrolą harmonogramu aktywnego w momencie utworzenia zadania, czyli harmonogramu ziarna.

  3. Task.ConfigureAwait with false jest jawnym interfejsem API umożliwiającym ucieczkę bieżącego harmonogramu zadań. Kod po oczekiwaniu na Task wykonuje się w harmonogramie TaskScheduler.Default (puli wątków platformy .NET), przez co zostaje przerwane jednowątkowe wykonywanie ziarna.

    Uwaga

    Ogólnie rzecz biorąc, nigdy nie używaj ConfigureAwait(false) bezpośrednio w kodzie ziarna.

  4. Metody z podpisem async void nie powinny być używane z ziarnami. Są one przeznaczone dla graficznych procedur obsługi zdarzeń interfejsu użytkownika. async void Metoda może natychmiast spowodować awarię obecnego procesu, jeśli pozwoli na ucieczkę wyjątku, bez możliwości jego obsłużenia. Dotyczy to również List<T>.ForEach(async element => ...) oraz każdej innej metody akceptującej Action<T>, ponieważ asynchroniczny delegat przekształca się w delegata async void.

Task.Factory.StartNew i async delegatów

Zwykle zaleca się używanie Task.Run zamiast Task.Factory.StartNew do planowania zadań w języku C#. Szybkie wyszukiwanie Task.Factory.StartNew w Internecie sugeruje , że jest to niebezpieczne i faworyzowanie Task.Run jest zawsze zalecane. Jednak aby pozostać w modelu wykonywania ziarna w jednym wątku, Task.Factory.StartNew należy użyć. Jak więc używać go poprawnie? Zagrożeniem związanym z Task.Factory.StartNew() jest brak natywnego wsparcia dla delegatów asynchronicznych. Oznacza to, że kod podobny var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync) jest prawdopodobnie usterką. notIntendedTask nie jest zadaniem, które kończy się, gdy SomeDelegateAsync się zakończy. Zamiast tego zawsze odpakuj zwrócone zadanie: var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap().

Przykład: wiele zadań i harmonogram zadań

Poniżej przedstawiono przykładowy kod demonstrujący użycie TaskScheduler.Current, Task.Run i specjalnego niestandardowego harmonogramu, aby wyjść z kontekstu ziarna i jak do niego powrócić Orleans.

public async Task MyGrainMethod()
{
    // Grab the grain's task scheduler
    var orleansTS = TaskScheduler.Current;
    await Task.Delay(10_000);

    // Current task scheduler did not change, the code after await is still running
    // in the same task scheduler.
    Assert.AreEqual(orleansTS, TaskScheduler.Current);

    Task t1 = Task.Run(() =>
    {
        // This code runs on the thread pool scheduler, not on Orleans task scheduler
        Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
        Assert.AreEqual(TaskScheduler.Default, TaskScheduler.Current);
    });

    await t1;

    // We are back to the Orleans task scheduler.
    // Since await was executed in Orleans task scheduler context, we are now back
    // to that context.
    Assert.AreEqual(orleansTS, TaskScheduler.Current);

    // Example of using Task.Factory.StartNew with a custom scheduler to escape from
    // the Orleans scheduler
    Task t2 = Task.Factory.StartNew(() =>
    {
        // This code runs on the MyCustomSchedulerThatIWroteMyself scheduler, not on
        // the Orleans task scheduler
        Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
        Assert.AreEqual(MyCustomSchedulerThatIWroteMyself, TaskScheduler.Current);
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    scheduler: MyCustomSchedulerThatIWroteMyself);

    await t2;

    // We are back to Orleans task scheduler.
    Assert.AreEqual(orleansTS, TaskScheduler.Current);
}

Przykład: wywołanie funkcji grain w kodzie uruchomionym w wątku z puli wątków

Inny scenariusz obejmuje kod ziarna, który musi "przerwać" model planowania zadań dla ziarna i uruchomić go w wątku puli wątków (lub w kontekście niezwiązanym z ziarnem), ale nadal musi wywołać kolejne ziarno. Wywołania ziarna można wykonywać z kontekstów innych niż ziarna bez dodatkowej ceremonii.

Poniższy kod demonstruje wykonywanie wywołania na ziarnie z kodu uruchomionego wewnątrz ziarna, ale poza jego kontekstem.

public async Task MyGrainMethod()
{
    // Grab the Orleans task scheduler
    var orleansTS = TaskScheduler.Current;
    var fooGrain = this.GrainFactory.GetGrain<IFooGrain>(0);
    Task<int> t1 = Task.Run(async () =>
    {
        // This code runs on the thread pool scheduler,
        // not on Orleans task scheduler
        Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
        int res = await fooGrain.MakeGrainCall();

        // This code continues on the thread pool scheduler,
        // not on the Orleans task scheduler
        Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
        return res;
    });

    int result = await t1;

    // We are back to the Orleans task scheduler.
    // Since await was executed in the Orleans task scheduler context,
    // we are now back to that context.
    Assert.AreEqual(orleansTS, TaskScheduler.Current);
}

Praca z bibliotekami

Niektóre biblioteki zewnętrzne używane przez kod mogą używać ConfigureAwait(false) wewnętrznie. Użycie ConfigureAwait(false) jest dobrym i poprawnym rozwiązaniem w środowisku .NET podczas implementowania bibliotek ogólnego przeznaczenia. Nie jest to problem w pliku Orleans. Dopóki kod ziarna wywołujący metodę biblioteki oczekuje na wywołanie tej biblioteki w regularny sposób, kod ziarna jest poprawny. Wynik jest dokładnie taki, jak jest to pożądane: kod biblioteki uruchamia kontynuacje w domyślnym harmonogramie (wartość zwracana przez TaskScheduler.Default, która nie gwarantuje kontynuacji uruchamianych w wątku ThreadPool, ponieważ często są wykonywane w poprzednim wątku), podczas gdy kod ziarna działa w harmonogramie ziarna.

Innym często poruszanym pytaniem jest to, czy wywołania biblioteki wymagają wykonania poprzez Task.Run— czyli czy kod biblioteki wymaga jawnego odciążenia na element ThreadPool (np. await Task.Run(() => myLibrary.FooAsync())). Odpowiedź brzmi nie. Odciążanie kodu do ThreadPool nie jest konieczne, z wyjątkiem sytuacji, gdy kod biblioteki powoduje blokujące wywołania synchroniczne. Zwykle każda dobrze napisana i poprawna biblioteka asynchroniczna platformy .NET (metody zwracające Task i nazwane z sufiksem Async ) nie tworzy wywołań blokujących. Podsumowując, odciążanie czegokolwiek do ThreadPool nie jest potrzebne, chyba że podejrzewa się, że biblioteka asynchroniczna jest wadliwa lub celowo używa się synchronizującej biblioteki blokującej.

Zakleszczenia

Ponieważ ziarna wykonują pojedyncze wątki, zakleszczenie ziarna jest możliwe przez synchroniczne blokowanie w sposób wymagający odblokowywania wielu wątków. Oznacza to, że kod wywołujący dowolną z następujących metod i właściwości może zablokować element (grain), jeśli przekazane zadania nie zostały ukończone przed wywołaniem metody lub właściwości.

  • Task.Wait()
  • Task.Result
  • Task.WaitAny(...)
  • Task.WaitAll(...)
  • task.GetAwaiter().GetResult()

Unikaj tych metod w dowolnej usłudze o wysokiej współbieżności, ponieważ mogą one prowadzić do niskiej wydajności i niestabilności. Głodują platformę .NET ThreadPool , blokując wątki, które mogą wykonać przydatną pracę i wymagają ThreadPool wprowadzenia dodatkowych wątków do ukończenia. Podczas wykonywania kodu ziarna te metody mogą spowodować zakleszczenie ziarna, więc unikaj ich również w kodzie ziarna.

Jeśli pewna część pracy związanej z synchronizacją powyżej asynchronicznej jest nieunikniona, najlepiej jest przenieść tę pracę do oddzielnego schedulera. Najprostszym sposobem jest na przykład użycie metody await Task.Run(() => task.Wait()). Należy pamiętać, że zdecydowanie zaleca się unikanie pracy w trybie synchronizacja nad asynchronicznością, ponieważ szkodzi to skalowalności i wydajności aplikacji.

Podsumowanie: Praca z zadaniami w programie Orleans

Co próbujesz zrobić? Jak to zrobić
Uruchom pracę w tle w wątkach puli wątków platformy .NET. Nie są dozwolone żadne wywołania kodu ziarna ani ziarna. Task.Run
Uruchom zadanie asynchronicznego procesu roboczego z poziomu kodu ziarna z gwarancjami Orleans współbieżności opartymi na kolei (zobacz powyżej). Task.Factory.StartNew(WorkerAsync).Unwrap() (Unwrap)
Uruchom synchroniczne zadanie procesu roboczego z poziomu kodu ziarna z gwarancjami Orleans współbieżności opartej na kolei. Task.Factory.StartNew(WorkerSync)
Limity czasu wykonywania elementów roboczych Task.Delay + Task.WhenAny
Wywoływanie metody biblioteki asynchronicznej await wywołanie biblioteki
Korzystanie z polecenia async/await Normalny model programowania .NET Task-Async. Obsługiwane i zalecane
ConfigureAwait(false) Nie używaj kodu ziarna wewnętrznego. Dozwolone tylko wewnątrz bibliotek.