api -contrllery - async vs nie-async

0

czesc, pytanie chyba dość juniorskie, ale nadal nie mam jasności co do tego zagadnienia, mimo że nieraz używałem zarówno asynców w metodach kontrollera, jak i nie-asynców.

dlaczego niby miałaby być różnica (i czy rzeczywiście istnieje różnica) w takich prostych przykładach:

przykład 1 async:

controller:

[HttpPut]
async Task<IHttpActionResult> SaveSomeData(Dto dto)
=> Ok(await _service.SaveSomeData());

service:

async Task<int> SaveSomeData(Dto dto)
{
...
var id = await _repo.Add(entity);
return id;
}

repo:

async Task<int>Add(Entity entity)
{
 Entities.Add(entity);
await  _efDbContext.SaveChangesAsync()
return entity.Id;
}

a kod nie-async

controller:

[HttpPut]
IHttpActionResult SaveSomeData(Dto dto)
=> Ok(_service.SaveSomeData());

service:

int SaveSomeData(Dto dto)
{
...
var id =repo.Add(entity);
return id;
}

repo:

int Add(Entity entity)
{
 Entities.Add(entity);
 _efDbContext.SaveChanges()
return entity.Id;
}

Założenie jest takie, że nasz serwis jak i repo mogą być nie async- są wykorzystywane tylko w tej jedynej ścieżce - nie są używane przez inne serwisy, które równolegle będą zapisywać do różnych (albo nawet i tego jednego) repo i nie obchodzą ich wyniki (w tym wypadku ID stworzonych encji)

1

jak masz async, to wewnetrznie serwer moze wykonywac kilka requestow, jezeli nie, to musi poczekac na wynik. Wiecej tutaj

https://stackoverflow.com/questions/30566848/when-should-i-use-async-controllers-in-asp-net-mvc

a tutaj od microsoftu
https://docs.microsoft.com/en-us/previous-versions/aspnet/ee728598(v=vs.100)?redirectedfrom=MSDN

0

fasadin, podstawy czytałem ale troszkę nie rozumiem -gdy wykonam GET na moim endpoincie 2 krotnie (wysyłajac to samo żądanie jedno po drugim)
zarówno w przypadku async, jak i nie async, gdy zapnę się debugerem,
będzie mi przetwarzać to tak samo (czyli będę musiał po kolei każdy krok przerobić 2 razy)

czy dobrze to zrozumiałem z artykułów, że gdy pójdzie dużo requestów, to muszą one czekać bo nie ma wolnego wątku (np. w IIS) i w 1 przypadku ten wątek byłby zwoliony i dostępny gdy repo czekałoby na zapis w BD, a gdyby się zapisało to odzyskałoby wątek magicznie?! No ale jak mogłboby odzyskać wątek, skoro znów może juz nie być wątków (bo są przeznaczone na inne requesty) - wiec dla tego jednego konkretnego użytkownika operacja mogłaby trwać nawet dłużej...?

6

gdy wykonam GET na moim endpoincie 2 krotnie (wysyłajac to samo żądanie jedno po drugim)
zarówno w przypadku async, jak i nie async, gdy zapnę się debugerem,
będzie mi przetwarzać to tak samo (czyli będę musiał po kolei każdy krok przerobić 2 razy)

tak. Przetwarza tak samo, bo debuggery wymuszaja "wykonanie" na jednym watku tak bys mogl to debugowac (nie jestem w 100% pewny ale chyba dlatego pare lat temu umiejetnosc debugowania wielowatkowych aplikacji byla czasami jako wymaganie ;))

tylko, ze dwa requesty to nie jest duzo. Sprobuj tak z milionem requestow async i bez async powinienes zobaczyc wtedy roznice w czasie wykonywania. Rowniez pamietaj, ze build na Debug rozni sie z tym na Release, wiec testuj na Release

czy dobrze to zrozumiałem z artykułów, że gdy dużo requestów, to muszą one czekać bo nie ma wolnego wątku (np. w IIS)

prawie, wyobraz sobie ta sytuacje ktora napisalem powyzej. Przy milionie requestow nie-async IIS bedzie musial czekac na kazdy jeden az skonczy sie wykonywac
w przypadku drugim bedzie to na zasadzie yield return
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield
wiec moze odpalac kolejne requesty w innych watkach i nie musi czekac na tego co poprzednio przed nim sie wykonywal

, a gdyby się zapisało to odzyskałoby wątek magicznie?

prawie, po prostu normalnie zwraca rezultat swojej pracy, wtedy w zaleznosci od ustawien Twojego GC zbierze ten watek szybciej lub wolniej (tu wlasnie Twoja magia sie dzieje)

No ale jak mogłboby odzyskać wątek, skoro znów może juz nie być wątków

Odzyskujesz wątki z requestow ktore sie zakonczyly. Wiec nie ma takiej sytuacji ze odzyskujesz watek ktorego nie ma.

wiec dla tego jednego konkretnego użytkownika operacja mogłaby trwać nawet dłużej...?

w teorii tak, ale raczej to nie bedzie jeden konkretny uzytkownikow, a raczej ich pula (no chyba, ze magicznie bys trafial z limitem tak, ze wlasnie tylko ten jeden user nie moze byc obsluzony)

0

ok Fasadin dzięki :) !

3
fasadin napisał(a):

Przetwarza tak samo, bo debuggery wymuszaja "wykonanie" na jednym watku tak bys mogl to debugowac (nie jestem w 100% pewny ale chyba dlatego pare lat temu umiejetnosc debugowania wielowatkowych aplikacji byla czasami jako wymaganie ;))

"Wymuszają" o tyle, że interfejs debuggera zazwyczaj jest jednowątkowy, samej aplikacji to nie modyfikuje (a dokładniej: aplikacja może sprawdzać, czy jest pod debuggerem i wtedy inaczej wykonywać różne rzeczy, ale nie widziałem, żeby to dotyczyło zarządzania wątkami). Breakpoint jest dodawany jako modyfikacja kodu maszynowego w miejscu, więc jeżeli kilka wątków tam wejdzie, to wszystkie się zatrzymają i przykładowo w VS będziesz mógł się między nimi normalnie przełączać. Zrobienie Step Over znowu zmodyfikuje kod maszynowy w "następnej" linijce, więc aktualny wątek będzie działał dalej (przestanie być zatrzymany), VS przeskoczy automatycznie na inny wstrzymany wątek, a ten poprzedni w pewnym momencie znowu trafi na breakpoint i się zatrzyma.

3

Jeśli będziesz debugować aplikację, w której działa wiele wątków, to po niemal każdym F10/F11 będziesz skakał w pozornie przypadkowe miejsca kodu. Tak więc debugger nic nie wymusza poza tym, że pauzuje wszystkie wątki na czas zatrzymania któregokolwiek z nich na breakpoint'cie.

Jeśli chodzi o zalety async w IIS - ma pewną ustaloną maksymalną liczbę wątków do przetwarzania requestów. Liczbę tę można zwiększać (https://stackoverflow.com/questions/12304691/why-are-iis-threads-so-precious-as-compared-to-regular-clr-threads), więc raczej nie chodzi o uwalnianie wątków-workerów, żeby mogły się zająć przyjęciem kolejnego requesta (acz bardzo duża liczba wątków oznacza bąrdzo dużą liczbę przełączeń kontekstów, a że ta operacja kosztuje to nie chcemy wątków "zbyt dużo"). Chodzi o uwolnienie wątku w ogóle - całe I/O (dysk, sieć itp) nie wymaga wątku i async pozwala to wykorzystać, zamiast zmuszać wątek do bezproduktywnego czekania na zakończenie operacji.

0

dlaczego niby miałaby być różnica (i czy rzeczywiście istnieje różnica) w takich prostych przykładach:

Ogólnie mi znane są dwie motywacje, by tak pisać.

  1. Gdy tworzona aplikacja musi obsłużyć większe obciążenie pod kątem operacji IO, ale warto pamiętać, że sama asynchroniczność nie jest gwarancją wydajności, poza tym są też inne czynniki, które warto wziąźć pod uwagę:

That said, in many cases the web server won’t be the performance bottleneck. Web servers can be scaled horizontally, while many databases cannot, so the bottleneck often ends up being the database. The cost of an additional web server is small compared to the cost of a developer’s time, so it’s generally cheaper to spin up another web server than it is to change existing code. While brute forcing a problem in this way may seem inelegant, trading CPU-time for development time is usually a good deal.

https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html

  1. Asynchroniczność jest użyteczna, gdy masz serwer, który utrzymuje ciągłe połączenie z klientem. W takim wariancie typowe wątki szybko, by się wyczerpały, a z drugiej strony są pośrednie wyjścia, gdzie asynchroniczna część jest wydzieloną częścią projektu, która pod spodem może kierować zadania do workerów, które opierają się na blokujących wywołaniach.
0

hej to jeszcze jedno pytanie:
załóżmy że mam 1 wspólny interfejs dla dwóch klas, z czego 1 wykona to asynchronicznie - np. poprzez pobranie pewnych danych z bazy, a druga nie, bo (strzelam) skorzysta active directory

public interface IUsersService
{
Task<UserDto> GetUsers();
}

public class ADUsersService:IUsersService
{

async Task<UserDto> GetUsers()
=>
_adHelper.GetUsers(); //...brak await - przez co nazwa metody podkreśla się na zielono

}

public class UsersService:IUsersService
{

async Task<UserDto> GetUsers() 
=>await _repo.GetUsers();

}

czy w takim wypadku olewać to, że w jednej z implementacji nazwa metody podkresla na zielono (i wywola sie ona synchronicznie) czy moze zrobić tam coś w stylu:

async Task<UserDto> GetUsers()
{
await Task.Start( ()=> _adHelper.GetUsers());
}

5

To kompletnie bez sensu. Jeśli chcesz uniknąć warninga, a także tworzenia niepotrzebnej maszyny stanów dla tej metody, to możesz zrobić metodę nie-asynchroniczną zwracającą taska:

Task<UserDto> GetUsers()
{
    return Task.FromResult(_adHelper.GetUsers());
}

Słowo kluczowe async nie jest częścią sygnatury metody.

2

Asynchroniczność:

  1. Zwiększa liczbę obsłużonych requestów w jednostce czasu aka throughput (zakładając że bottleneck'iem jest IO - a zazwyczaj tak jest e.g. calle do innych serwisów lub DB).
  2. Nie zmiesza czasu trwania requestu (czekać i tak musimy, tylko bez blokowania wątku)
  3. Jest upierdliwa w użyciu bo solisz wszędzie async i await; A puryści do tego Async na końcu nazw metod. Jak cala apka jest async to jest to nadmiarowa i niepotrzebna informacja.

Pisanie nie-async codu to niestety walka z wiatrakami. Wiele lib'ek zakłada już że to właśnie async api jest tym domyślnym i albo ma gorsze sync api albo nawet wcale sync api nie wystawia.
Cała dokumentacja M$ oraz prawie wszystkie prezentacje, książki, wpisy itp. są o wersji async.
Prawda jest taka że async trzeba użyć choćby po to by mieć już święty spokój i z niczym nie walczyć.

Na początku async bardzo ssał. Dawno dawno temu bardzo prosto można było sobie zdeadlockować wątki w IIS. W .Net Core sporo poprawili. Takie zabawki jak https://github.com/benaadams/Ben.Demystifier ogarniają nawet call stacki (ostatnie wersje .NET Cora mają to już out of the box).

0

OK, to ja dodam pytanie od siebie. Czytam sobie MSDN i mam teraz zagwozdkę.

Do tej pory kontrolery pisałem w taki sposób i myślałem, że wszystko jest ok:

public class MyController: Controller
{
    public async Task<IActionResult> Foo()
    {
        var result = await myService.Foo();
        if(result)
            return Ok();
        else
            return BadRequest();
    }
}

A teraz czytam tutaj że powinienem posłużyć się specjalną klasą AsyncController.
Więc pojawia się pytanie, czy mój sposób też jest dobry (zwykły kontroller i async-await), czy powinienem używać AsyncController, żeby requesty były faktycznie asynchroniczne?

2

@Juhas zalezy jakiej wersji jezyka / frameworka uzywasz. AsyncController jest w mvc 2 a w nowych wersjach wszystkie controllery sa async

https://stackoverflow.com/questions/26939496/asynccontroller-missing-in-mvc-6

0

jeszcze pytanie
jest metoda async/await
inna metoda z niej korzysta, ale ona juz nie bedzie async, więc zeby poczekała na rezultat wrzucam:

void SomeMethod()
{
 someService.GetSthAsync().Result;
}

i dlaczego to powyżej czasem nie zadziała:
przypadek jest taki, że robię request do async controllera,
przy debugu wraca z metody, dochodzi do return Ok(result);
ale przeglądarka dalej czeka na rezultat.

O co w tym chodzi?

I drugie pytanie - czy poniższy kod jest właściwy?
Tworzę w nim 2 taski, które uderzają do 2 różnych serwisów (a tamte do 2 róznych serwerów (AD+Exchange) + wykonują niektóre operacje async)

public Result<IList<GroupDto>> GetGroups(string name)
        {
            var groups = new List<GroupDto>();
            var findGroupsTasks = new Task[]
            {

                new Task(() =>
                {
                        var exchangeGroups = _cloudADService.FindGroups(name);
                        if(exchangeGroups!=null && exchangeGroups.Any())
                            groups.AddRange(exchangeGroups);
                }),
                new Task(() =>
                {
                    var adGroups = _aDGroupsService.GetGroups(name);
                    if(adGroups.IsSuccess && adGroups.Value.Any())
                       groups.AddRange(adGroups.Value);
                })
            };
            foreach (var task in findGroupsTasks)
                task.Start();
            Task.WaitAll(findGroupsTasks);
            var result = Result.Ok<IList<GroupDto>>(groups);
            return result;
        }

I to działa (ale czy to jest dobrze napisane?),

I jsezcze kolejny przykład, z taskami + asynciem

    public async Task<IList<GroupDto>> GetGroupsOwnedBy(string email)
        {
            List<GroupDto> adGroups = null;
            List<GroupDto> exchangeGroups = null;

            var ownedGroupstasks = new Task[]
            {
                new Task(() =>
                {
                    try {
                        var userOwnedAD = _groupRepository.GetGroupsOwnedByUser(email).ToArray();
                        adGroups = new List<GroupDto>(userOwnedAD.Count());
                        if(userOwnedAD != null && userOwnedAD.Length >0)
                            adGroups.AddRange(userOwnedAD);
                    }
                    catch (Exception ex) { _logger.Error($"Could not get owned groups from ActiveDirectory, {ex.Message}"); }
                }),
                new Task(() =>
                {
                    try
                    {
                        var groups = _cloudADService.GetUserOwnedGroups(email);
                        var creationInfos = new List<GroupCreationDto>();
                        exchangeGroups = new List<GroupDto>(groups.Count());

                        var groupCreations =_groupCreationRepisotry.GetGroupCreationInfo(groups.Select(x=>x.GroupId).ToArray()).Result;


                        Parallel.ForEach(groups, (group) =>
                        {
                            var info = groupCreations.FirstOrDefault(x=>x.GroupId == group.GroupId);

                            var groupExists = GroupsHelper.GroupExists(info);
                            if(groupExists)
                            {
                                group.ExpirationDate = info?.ExpirationDate;
                                exchangeGroups.Add(group);
                            }
                            
                        });
                    
                    }
                    catch{}
                })
            };
            foreach (var task in ownedGroupstasks)
                task.Start();

            Task.WaitAll(ownedGroupstasks);
            var result = new List<GroupDto>();
            if (adGroups != null && adGroups.Any()) result.AddRange(adGroups);
            if (exchangeGroups != null && exchangeGroups.Any()) result.AddRange(exchangeGroups);


            return result;
        }

to powyższe również działa, ale nie działało gdy zamiast

  new Task(() =>
                {
                    try
                    {
                        var groups = _cloudADService.GetUserOwnedGroups(email);
                        var creationInfos = new List<GroupCreationDto>();
                        exchangeGroups = new List<GroupDto>(groups.Count());

                        var groupCreations =_groupCreationRepisotry.GetGroupCreationInfo(groups.Select(x=>x.GroupId).ToArray()).Result;

zrobiłem w ten sposób:

  new Task(async () =>
                {
                    try
                    {
                        var groups = _cloudADService.GetUserOwnedGroups(email);
                        var creationInfos = new List<GroupCreationDto>();
                        exchangeGroups = new List<GroupDto>(groups.Count());

                        var groupCreations =await _groupCreationRepisotry.GetGroupCreationInfo(groups.Select(x=>x.GroupId).ToArray());

Czy w ogóle dobrze to obsługuje?
Szczerze mówiąc dopiero od niedawna mam styczność z asyncami na poziomie controllerów i w ogóle asynchronicznością/wielowątkowością, itd

3
heyyou napisał(a):

i dlaczego to powyżej czasem nie zadziała:

W ASP.NET Core koncepcyjnie zadziała, o ile pod spodem nie dzieje się magia z kontekstami synchronizacji (ale w corze już jej niemal nie ma, co innego w starym ASP.NET). Niemniej czekanie w taki sposób jest do niczego, bo kompletnie traci zalety asynca. Musisz zrobić async do samej góry (i najlepiej ConfigureAwait(false) do samego dołu). Ponadto Ty piszesz, że robisz to na starym ASP.NET, więc przyjmij, że jest to zbrodnia i wystrzegaj się jak ognia. Da się robić takie rzeczy, ale trzeba dobrze rozumieć bebechy asynca.

heyyou napisał(a):

I to działa (ale czy to jest dobrze napisane?),

Nie, jest skopane. Nie twórz tasków przez konstruktor, tylko używaj Task.Run. Nie rób WaitAll, tylko przerób cały kod na async i zrób WhenAll + await. No i nie połykaj wyjątków.

heyyou napisał(a):

to powyższe również działa, ale nie działało gdy zamiast

Nie widzę na pierwszy rzut oka, dlaczego z awaitem nie działa, a z synchronicznym czekaniem tak. To bardzo fajny przykład, bo zazwyczaj jest na odwrót (z awaitem działa, a bez nie), więc jak możesz, to go obetnij ze zbędnych rzeczy i wrzuć tutaj cały kod. W każdym razie pewnie jak dodasz ConfigureAwait(false), to zadziała.

1 użytkowników online, w tym zalogowanych: 0, gości: 1