Přejít k navigační liště

Zdroják » JavaScript » Dart – Neznesiteľná ľahkosť asynchrónneho bytia

Dart – Neznesiteľná ľahkosť asynchrónneho bytia

Články JavaScript

Asynchrónnosť má niečo do seba. Imagine: žiadne thready, žiadne zamykanie objektov, žiadne deadlocks, livelocks. Žiadne webservery s vymrazenými 4 vláknami. Žiadne continuations for rescue (zakričte: fuj) a ďalšie podobné hacky. Žiadne problémy s neefektívne využitými zdrojmi. Kto však píše asynchrónny kód, vie, že táto selanka je len jednou časťou pravdy; asynchrónnosť vie niekedy poriadne skomplikovať život!

Nálepky:

Warmup

Začnime niečím na zahriatie. Definujme si funkciu ajax

import 'dart:async'; //potrebne pre Futures

Future<Map> ajax(Map req) {
  //future caka 1 sekundu, potom sa zacne vykonavat
  return new Future.delayed(new Duration(seconds: 1)).then( 
      (_){
        //factory konstruktor, vytvori shallow copy of req
        Map res = new Map.from(req); 
        res['data']++;
        return res;
      });
}

Funckia teda čaká jednu sekundu, potom pochrúme vstup a vráti response objekt (všetko Map-y) obalené vo futures. Názov ajax má asociovať dlhotrvajúcu operáciu a zároveň jemne pripomína predchádzajúci diel tohto seriálu. Garantujem vám, že pokiaľ takúto rutinu dáte BFC (bežný Franta kóder) a poviete mu, aby spravil tri na seba nadväzujúce ajax-y, napíše niečo takéto:

void main(List<String> args) {
  ajax({'data': 0, 'info': 'ahoj svet'}).then(
      (res){
        ajax(res).then(
            (res){
              ajax(res).then(
                  (res){
                     print(res); //vypise: {data: 3, info: ahoj svet}
                  });
            });
      });
}

Samozrejme teda, pokiaľ sa jedná o šikovného BFC, ktorý už do Futures aspoň trochu prenikol. Problémom hore uvedeného kódu je, že je odporný. Vieme ho nejako upratať? Našťasie áno, vďaka peknej vlastnosti – reťazenia futures – sa kód dá prepísať takto (do ukážky som pridal ešte jeden ajax)

  ajax({'data': 0, 'info': 'ahoj svet'}).then(
      (res) => ajax(res)
  ).then(
      (res) => ajax(res)
  ).then(
      (res) => ajax(res)
  ).then(
      (res){print(res);}
  );

Že vám to príde skášlenie v štýle „lepšie ako drótom do oka“? Prepísaním sme sa zbavili kučeravých zátvoriek (táto výhoda sa vytratí v okamihu, ako do then budete chcieť vložiť hocičo iné ako „oneliner“), oveľa dôležitejšie však je, že výsledný kód nie je príliš vnorený; všetky then-y sa viažu na top-level Future objekt, s ktorým sa postupne reťazia. Keď do dartu pridajú await, bude kód ešte krajší.

Testujeme

Otestujme, či náš ajax robí to, čo má. Už aj BFCs pochopili, že predtým, ako chceme testovať hodnotu vrátenú z ajaxu používať, treba si na ňu počkať; expect teda napíšeme do vnútra then:

void main() {

  setUp(){}

  test('ajax increments properly', () {
    ajax({'data': 0, 'info': 'ahoj svet'}).then(
        (res){expect(res['data'], equals(1));}
    );
  });
}

Náš test krásne funguje a vypíše „PASS: ajax increments properly“. So far so good, čo však keď upravíme expect na expect(res['data'], equals("no bloody way"));? Náš test stále pass-uje (je to proste kvalitný test, ktorý sa nenechá rozhádzať drobnosťami, akými sú chyby v kóde). V konzole síce uvidíme chybu, ktorú expect spôsobil, test sa však tvári ako bezproblémový.

Skúste sa vcítiť do test runnera (teda, obslužnej rutiny, ktorá sa stará a púšťanie testu). Spustili ste test, test v pokoji dobehol, vytvoril Future a skončil. Načo reportovať chybu? Test runner nemá ako vedieť, že treba čakať na výsledok nejakej Future! Tento fakt mu treba explicitne odkomunikovať použitím expectAsync

  test('ajax increments properly v. 2', () {
    ajax({'data': 0, 'info': 'ahoj svet'}).then(expectAsync1(
        (res){expect(res['data'], "nbw");}
    ));
  });

V tejto verzii už test naozaj funguje (ako má). Poznamenajme ešte, že „1“ na konci expectAsync1 vyjadruje, koľko parametrov bude mať callback, na ktorý sa čaká. Štandardná unittest knižnica ponúka expectAsyncN pre N=0,1,2, kto chce viac, musí si napísať vlastnú implementáciu (dobrá inšpirácia je tu, dole) Potreba špecifikovať N vzniká z primalej dynamičnosti jazyka, škoda, že Dart nemá niečo ako sú Pythonie splats (o problémoch s typom callbacku ani nehovorím).

Okrem expectAsyncN existuje protectAsyncN a guardAsync, no napriek spamovaniu kóderov v Google nikomu z nás nie je jasné, čo by tieto funckie mali robiť, resp. robia; privítam hociaké rozumné vysvetlenie.

Inou možnosťou, ako test runnerovi oznámiť, že treba počkať na výsledok asynchrónnej operácie je priamo z testu vrátiť Future, runner potom čaká na jej skompletovanie.

Sync Futures

Predstavme si, že chceme nasledovnú funckionalitu:

dynamic processData(String key){
  var res;
  if(cache.containsKey(key)){ //mame hodnotu v cache?
    res = cache[key]; // (*)
  } else {
    res = ajax(...) // ziskaj hodnotu zo servra
  }
  doSthWith(res);
  return res;
}

Teda, najprv zistíme, či je hodnota v cache, ak nie, vypýtame si ju zo servra. Problémom je, že na konci if-u nevieme, či v res sa nachádza priamo hodnota, alebo jej future. Riešenie? Nahradíme riadok (*) týmto:

res = new Future.value(cache[key]);

prípadne

res = new Future.sync(() => cache[key]);

a je to. Na konci if máme istotu, že res je Future, v prípade že sme hodnotu vytiahli z cache, je to Future, ktorá completuje hneď v najbližšom event loope (pozor, nie úplne hneď! Toto opozdenie bude asi sotva markantné z hľadiska výkonu, treba však myslieť na to, že aj v tomto prípade sa callback v then vykoná až po synchrónne nasledujúcich príkazoch).

Okrem vyššie uvedeného use-case sú Future.sync vhodné v prípade, že chcete chytať výnimky v mixe synchrónneho a asynchrónneho kódu, pekná ukážka tu.

Prečo nefunguje try-catch-finally alebo God help us all

Na záver niečo, z čoho sa človeku robí nevoľno: Ak kód skonštruuje Future, ktorá vo svojom then callbacku vyhodí výnimku, dart VM sa zrúbe, nepomôže try-catch-finally okolo konštrukcie callbacku. Dobre to dokladá nasledovný príklad, kde skonštruovaná delayed future zhodí Timer („here I stand“ sa vypíše cca 10krát, program potom havaruje)

void main(List<String> args) {
  try{
    new Future.delayed(new Duration(seconds: 1), (){throw new Exception('uh oh');});
  }catch(_){}
  new Timer.periodic(new Duration(milliseconds: 100), (_) => print('here I stand'));
}

Nie že by t-c-f bolo pokazené, je to len tá istá logika, ako pri našom funkčnom-nefunkčnom teste: kód dobehol, žiadna výnimka nenastala (ak by áno, t-c-f by ju korektne spracovalo), Futures, a vôbec, budúcnosť, nie sú naša starosť, deň má dosť svojho trápenia!

Inými slovami, vďaka Futures môže hociaký 3rd party kód spôsobiť vyhodenie výnimky, ktorá položí celú našu Dart VM (snáď mi po tomto odpustíte ten patetický nadpis). Dart-isti si tento problém uvedomili a prišli s riešením s názvom runZoned. Pekná ukážka použitia je napríklad tu. Len čo sa vychytajú bugy podobné tomuto, bude pomocou runZoned možné dosiahnuť podobný luxus, ako pri písaní synchrónneho kódu dávajú try-catch-finally bloky.

Komentáře

Subscribe
Upozornit na
guest
4 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Radek Miček

No, není to moc pěkné. Budu-li Future chápat jako monádu, tak mohu využít syntaktickou podporu pro monády v některých jazycích a získat tak hezčí kód.

C# 5 má speciální podporu pro asynchronní kód – lze psát téměř normální kód.

Otázkou je, proč vlastně nepsat úplně normální kód a nenechat kompilátor / interpretr, aby ho vykonal asynchronně.

Ladislav Thon

Není to moc pěkné,ale v řadě jiných jazyků (ehm, JS, ehm) je to ještě horší :-)

V jakési experimentální větvi dart2js existovala (a možná ještě existuje, netuším) podpora pro C#-like await nad Future. A kromě http://dartbug.com/104 existuje ještě http://dartbug.com/7002. A i když se zatím neví, jak to nakonec dopadne, nějaká podpora přímo pro asynchronní kód v Dartu bude. Jednu možnost kdysi prezentoval Gilad, viz slajd č. 32 z https://www.dartlang.org/slides/2012/10/html5devconf/dart-today-and-beyond.pdf.

Pavel Dvořák

Bude seriál pokračovat? Třeba něčím o serveru v Dartu…

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.