Async/await hádanky - řešení

Ondřej Janáček       10.08.2014       C#, .NET       13005 zobrazení

Před týdnem jsem publikoval pár hádanek na async/await a dnes přidávám řešení ve formě správné odpovědi a možného vylepšení kódu.

Proč mi zase zamrzlo UI?

async void button1_Click(...)
{
    Action work = CPU work;
    await ScheduleAsync(work);
}

async Task ScheduleAsync(Action work)
{
    work();
}

Označením metody klíčovým slovem async a její zavolání s použitím await za Vás nové vlákno pro náročnou práci samo nevytvoří. Pro neblokující provedení operace jednoduše použijeme metodu Task.Run().

async void button1_Click(...)
{
    Action work = CPU work;
    await Task.Run(() => work);
}

Z tohoto příkladu si odneste následující 3 informace:

  1. async/await za Vás neřeší vláknovost
  2. await Vás vrací do stejného SynchronizationContextu (vlákna, chcete-li), ze kterého jste asynchronní operaci odstartovali
  3. To, jak je operace vyvolána, neříká nic o tom, jak se zpracuje skutečná práce.

Proč to proběhlo rychleji, než chci? - 1

...
Task.Delay(1000);
...

Zde stačí použít await. Takto jen zavoláme operaci, která nám vrátí Task, na jehož dokončení jsme zapomněli počkat a tak proběhl zbytek metody. Toto je změna oproti Thread.Sleep(1000);, což je blokující volání.

Proč to proběhlo rychleji, než chci? - 2

...
await Task.Factory.StartNew(
    async () => { await Task.Delay(1000); });
...

Tato hádanka je trochu záludnější, ale není to nic, s čím by Vám MSDN neporadilo. Pokud tuto informaci na MSDN hledáte, je nutné si uvědomit, že v tomto případě se nepoužije přetížení přijímající jediný parametr typu Action, ale Func<TResult>, protože asynchronní lambda vrací Task, nikoliv void. Podle popisu nám volání této metody vrátí Task<TResult>, na který můžeme čekat. Problém je tedy zřejmý, dostáváme zpět objekt typu Task<Task> a čekáme na dokončení toho venkovního, jehož volání končí okamžikem, kdy dostaneme Task z lambdy, tedy ihned. Zde jsou rovnou tři možná řešení.

await await Task.Factory.StartNew(
    async () => { await Task.Delay(1000); });
// nebo
await Task.Factory.StartNew(
    async () => { await Task.Delay(1000); }).Unwrap();
// nejlépe
await Task.Run(async () => { await Task.Delay(1000); });

Z tohoto příkladu si odneste následující informaci: pro startování nových asynchronních Tasků je v .NET 4.5 speciálně určená metoda Task.Run(), kterou se doporučuje používat v případě, že nepotřebujete některou z vlastností Task.Factory.StartNew. Zde je dokonce celý článek, který tyto dvě metody porovnává.

Tak kdy už to skončí? - 1

void button1_Click(...)
{
    FooAsync().Wait();
}

async Task FooAsync()
{
    await Task.Delay(1000);
}

Zde dojde k deadlocku, protože jak už jsem jednou psal, await vrací běhu programu do vlákna, ze kterého byla operace odstartována, zde tedy do UI vlákna. Po stisknutí tlačítka se zavolá FooAsync, kde program narazí na await, uloží kontext do stavového automatu, který se vytvoří, odstartuje operaci a vrátí se zpět do metody button1_Click, kde se zavolá blokující Wait(). V tuto chvíli tedy hlavní vlákno čeká pomocí Wait na dokončení Tasku a je zablokované. Po vteřině se dokončí operace Delay a stavový automat pošle informaci do hlavního vlákna, že skončil a že scheduler může naplánovat pokračování operaci v kontextu, který si automat načetl před odstartováním operace, tedy do hlavního vlákna. To je ale zrovna vytížené čekáním na Task a tak informace o dokončení stojí ve frontě.

Vyrobit takovýto problém je velmi snadné, pokud se konzument Vaší knihovny rozhodne, že trocha čekání na UI vlákně mu nevadí. To ale ještě neví, že se nedočká. Toto normálně funguje, pokud se FooAsync().Wait() zavolá v jiném než hlavním vlákně, které zůstává dostupné pro příjem zpráv. Na straně klienta můžeme problému snadno zabránit použitím await namísto Wait(). Pokud jste ale autor dané knihovny, která takovéto rozhraní poskytuje, je dobrým zvykem použít následující:

async Task FooAsync()
{
    await Task.Delay(1000).ConfigureAwait(false);
}

Tímto určíte, že po skončení operace se nemá pokračování plánovat do uloženého kontextu, ale raději se bude pokračovat v aktuálním. Umožníte tak uživateli, aby se potencionálnímu deadlocku vyhnul nehledě na to, na kterém vlákně operaci spustí.

Tak kdy už to skončí? - 2

async void button1_Click(...)
{
    var result = await Foo();    
}

Task<int> Foo()
{
    var tcs = new TaskCompletionSource<int>();
    Task.Run(delegate
    {
        int result = Bar();
        tcs.SetResult(result);
    });
    return tcs.Task;
}

Zde je opravdu vše v pořádku. Tedy, až do chvíle, kdy metoda Bar() vyhodí výjimku, kterou Task.Run() vesele sežere a nikomu nic neřekne. Operace sice doběhne, hlavní vlákno není blokované, ale výsledku se nedočkáme. Správně by to tedy mělo být takto:

async void button1_Click(...)
{
    try { int result = await Foo(); }
    catch (Exception ex) { ... }
}

Task<int> Foo()
{
    var tcs = new TaskCompletionSource<int>();
    Task.Run(delegate
    {
        try
        {
            int result = Bar();
            tcs.SetResult(result);
        }
        catch (Exception e) { tcs.SetException(e); }
    });
    return tcs.Task;
}

Z tohoto příkladu si odneste následující: nikdy neprogramujte jen s myšlenkou úspěchu, předpokládejte i neúspěch (pokud tedy metoda Bar() není Vaše a nevíte jistě, že chybu nehodí).

 

hodnocení článku

1 bodů / 1 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

Nové informácie

Všetky nové informácie sú pre mňa prínosom

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Jsem rád, že jste se dozvěděl něco nového. Děkuji za zprávu.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback