Udostępnij za pośrednictwem


Zadania zewnętrzne i ziarna

Zgodnie z projektem wszystkie podzadania zduplikowane z kodu ziarna (na przykład przy użyciu lub await ContinueWith Task.Factory.StartNew) będą wysyłane na taką samą aktywację TaskScheduler co zadanie nadrzędne i dlatego dziedziczą ten sam model wykonywania jednowątkowego co reszta kodu ziarna. Jest to główny punkt związany z wykonywaniem jednowątkowego współbieżności opartej na ziarnie.

W niektórych przypadkach może być konieczne "podzielenie" Orleans modelu planowania zadań i "wykonanie czegoś specjalnego", na przykład jawne wskazanie Task innego harmonogramu zadań lub platformy .NET ThreadPool. Przykładem takiego przypadku jest to, że kod ziarna musi wykonać synchroniczne zdalne wywołanie blokujące (np. zdalne we/wy). Wykonanie tego blokującego wywołania w kontekście ziarna spowoduje zablokowanie ziarna 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 i sprzężenia (await) ukończenia tego wykonania i kontynuować w kontekście ziarna. Oczekujemy, że ucieczka z Orleans harmonogramu będzie bardzo zaawansowanym i rzadko wymaganym scenariuszem użycia poza "normalnymi" wzorcami użycia.

Interfejsy API oparte na zadaniach

  1. await, TaskFactory.StartNew (patrz poniżej), Task.ContinueWith, Task.WhenAny, Task.WhenAll, Task.Delay wszystkie z uwzględnieniem bieżącego harmonogramu zadań. Oznacza to, że użycie ich w domyślny sposób bez przekazywania innego TaskSchedulerelementu spowoduje ich wykonanie w kontekście ziarna.

  2. Zarówno Task.Run , jak i endMethod pełnomocnik TaskFactory.FromAsync nie przestrzegają bieżącego harmonogramu zadań. Obaj używają TaskScheduler.Default harmonogramu, który jest harmonogramem zadań puli wątków platformy .NET. W związku z tym kod wewnątrz Task.Run i wewnątrz Task.Factory.FromAsync endMethod będzie zawsze uruchamiany w puli wątków platformy .NET poza modelem wykonywania jednowątkowego dla Orleans ziarna. Jednak każdy kod po await Task.Run kodzie lub await Task.Factory.FromAsync zostanie uruchomiony z powrotem w harmonogramie w momencie utworzenia zadania, który jest harmonogramem ziarna.

  3. Task.ConfigureAwait with false jest jawnym interfejsem API umożliwiającym ucieczkę bieżącego harmonogramu zadań. Spowoduje to wykonanie kodu po oczekiwanym zadaniu w harmonogramie TaskScheduler.Default , czyli puli wątków platformy .NET, co spowoduje przerwanie jednowątkowego wykonywania ziarna.

    Uwaga

    Ogólnie rzecz biorąc , nigdy nie należy używać 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 ulegać awarii bieżącego procesu, jeśli zezwalają na wyjątek ucieczki bez możliwości obsługi wyjątku. Dotyczy to również List<T>.ForEach(async element => ...) i każdej innej metody, która akceptuje Action<T>element , ponieważ pełnomocnik asynchroniczny zostanie coerced do delegata async void .

Task.Factory.StartNew i async delegatów

Zwykle zaleca się planowanie zadań w dowolnym programie języka C# na Task.Run rzecz Task.Factory.StartNewprogramu . Szybkie wyszukiwanie google na temat użycia Task.Factory.StartNew sugeruje, że jest niebezpieczne i że powinien zawsze faworyzować Task.Run. Ale jeśli chcemy pozostać w modelu wykonywania ziarna jednowątkowego dla naszego ziarna, musimy go użyć, więc jak to zrobić poprawnie? Niebezpieczeństwo użycia Task.Factory.StartNew() polega na tym, że nie obsługuje natywnie delegatów asynchronicznych. Oznacza to, że prawdopodobnie jest to usterka: var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync). notIntendedTasknie jest zadaniem, które kończy się w takim przypadkuSomeDelegateAsync. Zamiast tego należy zawsze odpakowywać zwrócone zadanie: var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap().

Przykład wielu zadań i harmonogramu zadań

Poniżej przedstawiono przykładowy kod, który demonstruje użycie , i specjalnego niestandardowego TaskScheduler.CurrentTask.Runharmonogramu, aby uciec z Orleans kontekstu ziarna i jak wrócić do niego.

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łania ziarna z kodu uruchamianego w puli wątków

Innym scenariuszem jest fragment kodu ziarna, który musi "rozbić" model planowania zadań ziarna i uruchomić w puli wątków (lub inny kontekst, inny niż ziarno), ale nadal musi wywołać inne ziarno. Wywołania ziarna można wykonywać z kontekstów innych niż ziarna bez dodatkowej ceremonii.

Poniżej przedstawiono kod, który pokazuje, jak można wykonać wywołanie ziarna z fragmentu kodu uruchamianego wewnątrz ziarna, ale nie w kontekście ziarna.

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, których używa kod, mogą używać ConfigureAwait(false) wewnętrznie. Dobrym i poprawnym rozwiązaniem jest użycie platformy ConfigureAwait(false) .NET podczas implementowania bibliotek ogólnego przeznaczenia. Nie jest to problem w pliku Orleans. O ile kod w ziarnie, który wywołuje metodę biblioteki, oczekuje na wywołanie biblioteki z regularnym awaitkodem ziarna jest poprawny. Wynik będzie dokładnie taki, jak jest to pożądane — kod biblioteki będzie uruchamiał kontynuacje w domyślnym harmonogramie (wartość zwrócona przez TaskScheduler.Defaultelement , która nie gwarantuje, że kontynuacje będą uruchamiane w ThreadPool wątku, ponieważ kontynuacje są często wbudowane w poprzednim wątku), podczas gdy kod ziarna będzie uruchamiany w harmonogramie ziarna.

Innym często zadawanym pytaniem jest to, czy istnieje potrzeba wykonania wywołań biblioteki za Task.Runpomocą — czyli czy istnieje potrzeba jawnego odciążania kodu biblioteki do ThreadPool (aby kod ziarna to zrobić Task.Run(() => myLibrary.FooAsync())). Odpowiedź brzmi nie. Nie ma potrzeby odciążania żadnego kodu ThreadPool z wyjątkiem przypadku kodu biblioteki, który tworzy blokujące wywołania synchroniczne. Zwykle każda dobrze napisana i poprawna biblioteka asynchronicka platformy .NET (metody zwracające Task i są nazwane sufiksem Async ) nie powoduje wywołań blokujących. W związku z tym nie ma potrzeby odciążania niczego, ThreadPool chyba że podejrzewasz, że biblioteka asynchroniczna jest usterka lub jeśli celowo używasz synchronicznej biblioteki blokującej.

Zakleszczenia

Ponieważ ziarna są wykonywane w sposób jednowątkowy, istnieje możliwość zakleszczenia ziarna przez synchroniczne blokowanie w sposób, który wymagałby odblokowania wielu wątków. Oznacza to, że kod, który wywołuje dowolną z następujących metod i właściwości, może zakleszczać ziarno, jeśli podane zadania nie zostały jeszcze ukończone przez czas wywołania metody lub właściwości:

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

Te metody należy unikać w dowolnej usłudze o wysokiej współbieżności, ponieważ mogą one prowadzić do niskiej wydajności i niestabilności przez głodowanie platformy .NET ThreadPool przez blokowanie wątków, które mogą wykonywać przydatne zadania i wymagają od platformy .NET ThreadPool wstrzyknięcia dodatkowych wątków, aby można je było ukończyć. Podczas wykonywania kodu ziarna te metody, jak wspomniano powyżej, mogą spowodować zakleszczenie ziarna i dlatego należy unikać ich również w kodzie ziarna.

Jeśli nie można uniknąć pewnej pracy synchronizacji za pośrednictwem asynchronicznego , najlepiej przenieść ją do oddzielnego harmonogramu. Najprostszym sposobem, aby to zrobić, jest await Task.Run(() => task.Wait()) użycie na przykład. Należy pamiętać, że zdecydowanie zaleca się unikanie pracy synchronizacji za pośrednictwem asynchronicznego , ponieważ, jak wspomniano powyżej, spowoduje to spadek skalowalności i wydajności aplikacji.

Podsumowanie pracy 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.