Asynchronní do hloubkyAsync in depth

Zápis vázané na vstupně-výstupní operace a procesor asynchronní kód je jednoduchý pomocí modelu .NET úkolově orientovanou asynchronní.Writing I/O- and CPU-bound asynchronous code is straightforward using the .NET Task-based async model. Model je zveřejněný prostřednictvím Task a Task<T> typy a async a await klíčová slova v jazyce C# a Visual Basic.The model is exposed by the Task and Task<T> types and the async and await keywords in C# and Visual Basic. (Specifické pro jazyk prostředky se nacházejí v viz také části.) Tento článek vysvětluje, jak použít operátory async .NET a poskytuje podrobné informace o asynchronní framework použitá na pozadí.(Language-specific resources are found in the See also section.) This article explains how to use .NET async and provides insight into the async framework used under the covers.

Úlohy a úkolů<T>Task and Task<T>

Úkoly jsou konstrukce používaný k implementaci, která se označuje jako Promise modelu Concurrency.Tasks are constructs used to implement what is known as the Promise Model of Concurrency. Stručně řečeno nabízejí, že jste "příslib", které pracují se dokončit později, umožňuje zajistěte ve spolupráci se příslib pomocí rozhraní API pro vyčištění.In short, they offer you a "promise" that work will be completed at a later point, letting you coordinate with the promise with a clean API.

  • Task představuje jednu operaci, která nevrací hodnotu.Task represents a single operation which does not return a value.
  • Task<T> představuje jednu operaci, která vrátí hodnotu typu T.Task<T> represents a single operation which returns a value of type T.

Je důležité o úlohy jako abstrakce práce děje asynchronně, a není abstrakci přes dělení na vlákna.It’s important to reason about tasks as abstractions of work happening asynchronously, and not an abstraction over threading. Ve výchozím nastavení se úkoly spustí v aktuální vlákno a delegátem práci do operačního systému, podle potřeby.By default, tasks execute on the current thread and delegate work to the Operating System, as appropriate. Volitelně můžete úlohy můžete výslovně požadovány ke spuštění v samostatném vlákně prostřednictvím Task.Run rozhraní API.Optionally, tasks can be explicitly requested to run on a separate thread via the Task.Run API.

Úlohy vystavit protokol rozhraní API pro monitorování, čeká na a přístup k výsledku hodnotu (v případě třídy Task<T>) úlohy.Tasks expose an API protocol for monitoring, waiting upon and accessing the result value (in the case of Task<T>) of a task. Integrace jazyka s await – klíčové slovo, poskytuje vyšší úroveň abstrakce pro používání úloh.Language integration, with the await keyword, provides a higher-level abstraction for using tasks.

Pomocí await umožňuje vaše aplikace nebo služba provádět užitečnou práci, zatímco úloha běží získávání řízení volajícímu, až po dokončení úkolu.Using await allows your application or service to perform useful work while a task is running by yielding control to its caller until the task is done. Váš kód nemusí spoléhat na zpětná volání nebo událostí, které pokračovat v provádění po dokončení úlohy.Your code does not need to rely on callbacks or events to continue execution after the task has been completed. Jazyk a úlohy integrace rozhraní API, které udělá za vás.The language and task API integration does that for you. Pokud používáte Task<T>, await – klíčové slovo bude kromě "rozbalení" hodnota vrácená po dokončení úkolu.If you’re using Task<T>, the await keyword will additionally "unwrap" the value returned when the Task is complete. Podrobnosti o tom, jak to funguje jsou vysvětleny níže.The details of how this works are explained further below.

Další informace o úlohách a různé způsoby, jak pracovat s nimi v úkolově orientovanou asynchronní vzor (TAP) tématu.You can learn more about tasks and the different ways to interact with them in the Task-based Asynchronous Pattern (TAP) topic.

Prozkoumejte podrobněji úkoly pro vstupně-výstupní operaceDeeper Dive into Tasks for an I/O-Bound Operation

Následující část popisuje, co se stane s volání typické asynchronní vstupně-výstupních operací 10 000 stopy zobrazení.The following section describes a 10,000 foot view of what happens with a typical async I/O call. Začněme několik příkladů.Let's start with a couple examples.

První příklad volá asynchronní metodu a vrátí aktivní úkol, pravděpodobně zatím k dokončení.The first example calls an async method and returns an active task, likely yet to complete.

public Task<string> GetHtmlAsync()
{
    // Execution is synchronous here
    var client = new HttpClient();
    
    return client.GetStringAsync("https://www.dotnetfoundation.org");
}

Druhý příklad přidá použití async a await klíčová slova se má operace provést úlohu.The second example adds the use of the async and await keywords to operate on the task.

public async Task<string> GetFirstCharactersCountAsync(string url, int count)
{
    // Execution is synchronous here
    var client = new HttpClient();
    
    // Execution of GetFirstCharactersCountAsync() is yielded to the caller here
    // GetStringAsync returns a Task<string>, which is *awaited*
    var page = await client.GetStringAsync("https://www.dotnetfoundation.org");
    
    // Execution resumes when the client.GetStringAsync task completes,
    // becoming synchronous again.
    
    if (count > page.Length)
    {
        return page;
    }
    else
    {
        return page.Substring(0, count);
    }
}

Volání GetStringAsync() volání prostřednictvím nižší úrovně knihovny .NET (například voláním ostatních metod asynchronní) dokud nedosáhne P/Invoke spolupráce volání do nativního síťové knihovny.The call to GetStringAsync() calls through lower-level .NET libraries (perhaps calling other async methods) until it reaches a P/Invoke interop call into a native networking library. Nativní knihovna může následně volání do volání rozhraní API systému (například write() na soket v Linuxu).The native library may subsequently call into a System API call (such as write() to a socket on Linux). Objekt úlohy se vytvoří spravované/nativní hranice, případně pomocí TaskCompletionSource.A task object will be created at the native/managed boundary, possibly using TaskCompletionSource. Objekt úlohy se předává vrstvy, pravděpodobně provozovaná nebo přímo vrácená, nakonec se vrátí volajícímu počáteční.The task object will be passed up through the layers, possibly operated on or directly returned, eventually returned to the initial caller.

V druhém příkladu výše Task<T> objektu bude vrácen z GetStringAsync.In the second example above, a Task<T> object will be returned from GetStringAsync. Použití await – klíčové slovo způsobí, že metoda vrátí objekt nově vytvořené úlohy.The use of the await keyword causes the method to return a newly created task object. Ovládací prvek vrátí volajícímu z tohoto umístění v GetFirstCharactersCountAsync metody.Control returns to the caller from this location in the GetFirstCharactersCountAsync method. Metody a vlastnosti úloh<T> objektu povolení volajícím můžete sledovat průběh úlohy, která bude dokončena, pokud byl proveden v GetFirstCharactersCountAsync zbývající kód.The methods and properties of the Task<T> object enable callers to monitor the progress of the task, which will complete when the remaining code in GetFirstCharactersCountAsync has executed.

Po volání rozhraní API systému žádost je nyní v prostoru jádra zajistit jeho způsob, jak síťový subsystém operačního systému (například /net v jádro Linuxu).After the System API call, the request is now in kernel space, making its way to the networking subsystem of the OS (such as /net in the Linux Kernel). Tady operačního systému bude zpracovávat síťové žádosti asynchronně.Here the OS will handle the networking request asynchronously. Podrobnosti se může lišit v závislosti na operační systém používá (volání ovladače zařízení může být naplánovaná jako signál odesílaných zpět do modulu runtime nebo volání ovladače zařízení se dají vytvořit a pak signál zaslal zpět), ale nakonec bude informován modulu runtime síťové žádosti se v průběhu.Details may be different depending on the OS used (the device driver call may be scheduled as a signal sent back to the runtime, or a device driver call may be made and then a signal sent back), but eventually the runtime will be informed that the networking request is in progress. V současné době práce pro ovladače zařízení budou být buď plánované, probíhá nebo už skončila (žádost je už si "při přenosu") – ale vzhledem k tomu, že to vše proběhne asynchronně, ovladače zařízení je schopná zpracovat okamžitě něco jiného!At this time, the work for the device driver will either be scheduled, in-progress, or already finished (the request is already out "over the wire") - but because this is all happening asynchronously, the device driver is able to immediately handle something else!

V operační systém Windows například vlákno provede volání k ovladači zařízení sítě a vyzve ho k provedení této operace sítě prostřednictvím přerušení žádosti paketů (IRP), která představuje operaci.For example, in Windows an OS thread makes a call to the network device driver and asks it to perform the networking operation via an Interrupt Request Packet (IRP) which represents the operation. Ovladače zařízení obdrží kontrolní, provede volání do sítě, označí IRP jako "čekající na vyřízení" a vrátí zpět do operačního systému.The device driver receives the IRP, makes the call to the network, marks the IRP as "pending", and returns back to the OS. Protože vlákna operačního systému nyní ví, že je kontrolní "čekající na vyřízení", nemá žádné další práci pro tuto úlohu a "vrátí" tak, aby je možné provádět jinou práci.Because the OS thread now knows that the IRP is "pending", it doesn't have any more work to do for this job and "returns" back so that it can be used to perform other work.

Pokud je požadavek splněn a data se vrátí zpět prostřednictvím ovladače zařízení, upozorní procesoru nová data přijatá přes přerušení.When the request is fulfilled and data comes back through the device driver, it notifies the CPU of new data received via an interrupt. Získá zpracování této přerušení se budou lišit v závislosti na operační systém, ale nakonec data se předají pomocí operačního systému dokud nedosáhne volání interop systému (třeba v systému Linux obslužné rutiny přerušení se naplánuje dolní polovinu IRQ předávání dat operačního systému asynchronně).How this interrupt gets handled will vary depending on the OS, but eventually the data will be passed through the OS until it reaches a system interop call (for example, in Linux an interrupt handler will schedule the bottom half of the IRQ to pass the data up through the OS asynchronously). Všimněte si, že tento také probíhá asynchronně!Note that this also happens asynchronously! Výsledkem je zařazena do fronty až další vlákno k dispozici je možné provést asynchronní metody a "rozbalení" výsledek dokončené úlohy.The result is queued up until the next available thread is able execute the async method and "unwrap" the result of the completed task.

V průběhu celý tento proces hlavní, co vyplývá je, že žádné vlákno je vyhrazen pro spuštění úlohy.Throughout this entire process, a key takeaway is that no thread is dedicated to running the task. I když práce provádí v kontextu (to znamená, že operační systém nemá předat data do ovladače zařízení a reagovat na přerušení), je vyhrazený pro žádné vlákno čekání pro data z požadavku k téhle akci vrátit.Although work is executed in some context (that is, the OS does have to pass data to a device driver and respond to an interrupt), there is no thread dedicated to waiting for data from the request to come back. To umožňuje zpracovat mnohem větší objem práce spíše než čekání na některé volání vstupně-výstupních operací na dokončení systému.This allows the system to handle a much larger volume of work rather than waiting for some I/O call to finish.

I když výše může jevit jako toho ještě hodně udělat, když se měří se počtem skutečný čas, miniscule k je čas potřebný k vykonávají samotnou práci vstupně-výstupních operací.Although the above may seem like a lot of work to be done, when measured in terms of wall clock time, it’s miniscule compared to the time it takes to do the actual I/O work. I když vůbec ne přesné potenciální časovou osu pro takové volání bude vypadat takto:Although not at all precise, a potential timeline for such a call would look like this:

0-1————————————————————————————————————————————————–2-30-1————————————————————————————————————————————————–2-3

  • Doba trvání z bodů 0 k 1 všechno, co je až do asynchronní metody vrací řízení volajícímu.Time spent from points 0 to 1 is everything up until an async method yields control to its caller.
  • Doba trvání z bodů 1 k 2 čas strávený vstupně-výstupní operace se žádné náklady.Time spent from points 1 to 2 is the time spent on I/O, with no CPU cost.
  • A konečně, doba trvání z bodů 2 k 3 předává řízení zpět (a potenciálně hodnotu) do asynchronní metody, v tomto okamžiku je znovu prováděna.Finally, time spent from points 2 to 3 is passing control back (and potentially a value) to the async method, at which point it is executing again.

Co to znamená pro scénář serveru?What does this mean for a server scenario?

Tento model funguje dobře s úlohou scénář typický server.This model works well with a typical server scenario workload. Vzhledem k tomu, že neexistují žádná vlákna vyhrazený pro blokování nedokončené úkoly, fondu vláken server služby mnohem větší objem webových požadavků.Because there are no threads dedicated to blocking on unfinished tasks, the server threadpool can service a much higher volume of web requests.

Vezměte v úvahu dva servery: ten, který spouští asynchronní kód a ten, který se nepodporuje.Consider two servers: one that runs async code, and one that does not. Pro účely tohoto příkladu má každý server jenom 5 vlákna, které jsou k dispozici pro žádosti o služby.For the purpose of this example, each server only has 5 threads available to service requests. Všimněte si, že tato čísla jsou imaginarily malé a slouží pouze v rámci Demonstrativní.Note that these numbers are imaginarily small and serve only in a demonstrative context.

Předpokládejme, že oba servery přijímat 6 souběžných požadavků.Assume both servers receive 6 concurrent requests. Každý požadavek provádí vstupně-výstupní operace.Each request performs an I/O operation. Server bez asynchronní kód má zařadit do fronty požadavek 6 až jedno z 5 vláken mít dokončené práce vstupně-výstupní zapsán odpovědi.The server without async code has to queue up the 6th request until one of the 5 threads have finished the I/O-bound work and written a response. V okamžiku, 20. prosincem požadavek odeslaný v, může server spustit zpomalit, protože fronta je stále příliš dlouhý.At the point that the 20th request comes in, the server might start to slow down, because the queue is getting too long.

Server s asynchronní kód spuštěný na ní stále fronty 6 požadavek, ale protože používá async a await, každý z jeho vlákna jsou uvolněna při spuštění vstupně-výstupní pracovní místo po dokončení.The server with async code running on it still queues up the 6th request, but because it uses async and await, each of its threads are freed up when the I/O-bound work starts, rather than when it finishes. Ve čas 20. prosincem žádost obsahuje ve frontě příchozích požadavků bude mnohem menší (pokud ho neobsahuje nic vůbec), a nebude server zpomalovat.By the time the 20th request comes in, the queue for incoming requests will be far smaller (if it has anything in it at all), and the server won't slow down.

Přestože je toto contrived příklad, funguje ve velmi podobně jako ochrana čipem v reálném světě.Although this is a contrived example, it works in a very similar fashion in the real world. Ve skutečnosti můžete očekávat, že server moct zpracovávat další požadavky na použití jednotkách o řád async a await než pokud to bylo vyhradíte vlákno pro každý požadavek přijme.In fact, you can expect a server to be able to handle an order of magnitude more requests using async and await than if it were dedicating a thread for each request it receives.

Co to znamená pro scénář klienta?What does this mean for client scenario?

Největší zisk pro používání async a await pro klienta je zvýšení rychlosti odezvy aplikace.The biggest gain for using async and await for a client app is an increase in responsiveness. I když responzivní aplikace můžete vytvořit pomocí ruční vytváření podřízeného procesu vlákna, v rámci vytváření podřízeného procesu vlákna je náročná operace vzhledem k použití pouze async a await.Although you can make an app responsive by spawning threads manually, the act of spawning a thread is an expensive operation relative to just using async and await. Zejména u něco jako mobilní hru vliv na vlákně UI při co kde jde o vstupně-výstupních operací je velmi důležité.Especially for something like a mobile game, impacting the UI thread as little as possible where I/O is concerned is crucial.

Důležitější je protože pracovní vstupně-výstupní stráví prakticky žádný čas na CPU, vyhradíte celé vlákno procesor k provádění i neziskovky jakékoli užitečné práce by špatné využití prostředků.More importantly, because I/O-bound work spends virtually no time on the CPU, dedicating an entire CPU thread to perform barely any useful work would be a poor use of resources.

Kromě toho, kterému dodává práci na vlákno uživatelského rozhraní (např. aktualizace uživatelského rozhraní) je velmi jednoduchý s async metody a nevyžaduje další práce (jako je například volání delegáta bezpečným pro vlákno).Additionally, dispatching work to the UI thread (such as updating a UI) is very simple with async methods, and does not require extra work (such as calling a thread-safe delegate).

Dozvědět více o úkolu a úkolu<T> pro operace vázané na procesorDeeper Dive into Task and Task<T> for a CPU-Bound Operation

Vázané na procesor async kód je trochu jiná než vstupně-výstupní async kódu.CPU-bound async code is a bit different than I/O-bound async code. Protože práce se provádí na CPU, neexistuje žádný způsob, jak překonat vyhradíte vlákno k výpočtu.Because the work is done on the CPU, there's no way to get around dedicating a thread to the computation. Použití async a await poskytuje čistý způsob, jak pracovat s pozadí vlákna a zachovat responzivní volajícímu asynchronní metody.The use of async and await provides you with a clean way to interact with a background thread and keep the caller of the async method responsive. Všimněte si, že to neposkytuje ochranu pro sdílená data.Note that this does not provide any protection for shared data. Pokud používáte sdílených dat, je stále potřeba použití strategie příslušné synchronizace.If you are using shared data, you will still need to apply an appropriate synchronization strategy.

Tady je přehled 10 000 stopy asynchronního volání vázané na procesor:Here's a 10,000 foot view of a CPU-bound async call:

public async Task<int> CalculateResult(InputData data)
{
    // This queues up the work on the threadpool.
    var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data));
    
    // Note that at this point, you can do some other work concurrently,
    // as CalculateResult() is still executing!
    
    // Execution of CalculateResult is yielded here!
    var result = await expensiveResultTask;
    
    return result;
}

CalculateResult() provede na vlákno, byla volána pro.CalculateResult() executes on the thread it was called on. Když volá Task.Run, se zařadí do fronty náročná operace vázané na procesor DoExpensiveCalculation(), ve fondu vláken a přijímá Task<int> zpracovat.When it calls Task.Run, it queues the expensive CPU-bound operation, DoExpensiveCalculation(), on the thread pool and receives a Task<int> handle. DoExpensiveCalculation() Nakonec běží souběžně na další dostupný vlákně, pravděpodobně na další Procesorové jádro.DoExpensiveCalculation() is eventually run concurrently on the next available thread, likely on another CPU core. Je možné provádět souběžné práce při DoExpensiveCalculation() je zaneprázdněný v jiném vlákně, protože vlákno, které volá CalculateResult() stále probíhá.It's possible to do concurrent work while DoExpensiveCalculation() is busy on another thread, because the thread which called CalculateResult() is still executing.

Jednou await nalezen, provádění CalculateResult() se vrátil volajícímu, což jiné práce s aktuálním vláknem při DoExpensiveCalculation() odchází si výsledek.Once await is encountered, the execution of CalculateResult() is yielded to its caller, allowing other work to be done with the current thread while DoExpensiveCalculation() is churning out a result. Po dokončení, výsledek je ve frontě ke spuštění v hlavním vlákně.Once it has finished, the result is queued up to run on the main thread. Nakonec vrátí hlavního vlákna pro provádění CalculateResult(), kdy bude mít výsledek DoExpensiveCalculation().Eventually, the main thread will return to executing CalculateResult(), at which point it will have the result of DoExpensiveCalculation().

Proč asynchronní pomůže tady?Why does async help here?

async a await jsou doporučené správu práce vázané na procesor při potřebujete rychlost odezvy.async and await are the best practice managing CPU-bound work when you need responsiveness. Existuje více vzory pro použití modifikátoru async pomocí práce vázané na procesor.There are multiple patterns for using async with CPU-bound work. Je důležité si uvědomit, že je s malými náklady na použití modifikátoru async a se nedoporučuje úzkou smyčky for.It's important to note that there is a small cost to using async and it's not recommended for tight loops. To je na vás k určení, jak při psaní kódu kolem této nové funkci.It's up to you to determine how you write your code around this new capability.

Viz také:See also