Asynkronisera/vänta
I datorprogrammering är mönstret async /wait en syntaktisk egenskap hos många programmeringsspråk som gör att en asynkron , icke-blockerande funktion kan struktureras på ett sätt som liknar en vanlig synkron funktion. Det är semantiskt relaterat till begreppet en coroutine och implementeras ofta med liknande tekniker, och är främst avsett att ge möjligheter för programmet att exekvera annan kod i väntan på att en långvarig, asynkron uppgift ska slutföras, vanligtvis representerad av löften eller liknande datastrukturer. Funktionen finns i C# 5.0 , C++20 , Python 3.5, F# , Hack , Julia , Dart , Kotlin 1.1, Rust 1.39, Nim 0.9.4, JavaScript ES2017 , Swift 5.5 och Zig , med en del experimentellt arbete i tillägg, betaversioner och särskilda implementeringar av Scala .
Historia
F# lade till asynkrona arbetsflöden med väntpunkter i version 2.0 2007. Detta påverkade mekanismen för asynkronisering/vänta till C#.
Microsoft släppte en version av C# med async/await för första gången i Async CTP (2011). Och släpptes senare officiellt i C# 5 (2012).
Haskell huvudutvecklare Simon Marlow skapade async-paketet 2012.
Python lade till stöd för async/await med version 3.5 2015 och lade till 2 nya nyckelord, async
och await
.
TypeScript lade till stöd för async/await med version 1.7 2015.
Javascript lade till stöd för async/await under 2017 som en del av ECMAScript 2017 JavaScript-utgåvan.
Rust lade till stöd för async/await med version 1.39.0 2019 med 1 nytt nyckelord async
och ett lazy eval await-mönster.
C++ lade till stöd för async/await med version 20 2020 med 3 nya nyckelord co_return
, co_await
, co_yield
.
Swift lade till stöd för async/await med version 5.5 2021, och lade till 2 nya nyckelord async
och await
. Detta släpptes tillsammans med en konkret implementering av Actor-modellen med nyckelordet aktör
som använder async/await för att förmedla tillgång till varje aktör utifrån.
Exempel C#
C # -funktionen nedan, som laddar ner en resurs från en URI och returnerar resursens längd, använder det här asynkrona/vänta-mönster:
public async Task < int > FindPageSizeAsync ( Uri uri ) { var client = new HttpClient (); byte [] data = väntar på klient . GetByteArrayAsync ( uri ); returnera data . Längd ; }
- För det första indikerar nyckelordet
async
för C# att metoden är asynkron, vilket betyder att den kan använda ett godtyckligt antalawait-
uttryck och binder resultatet till ett löfte . - Returtypen,
Task<T>
, är C#s analog till begreppet ett löfte, och här indikeras att ha ett resultatvärde av typenint
. - Det första uttrycket som körs när denna metod anropas kommer att vara
new HttpClient().GetByteArrayAsync(uri) ,
vilket är en annan asynkron metod som returnerar enTask<byte[]>
. Eftersom den här metoden är asynkron kommer den inte att ladda ner hela satsen med data innan den returneras. Istället kommer den att börja nedladdningsprocessen med en icke-blockerande mekanism (som en bakgrundstråd ), och omedelbart returnera en olöst, oavvisadTask<byte[]>
till den här funktionen. - Med
await
nyckelordet kopplat tilluppgiften
kommer denna funktion omedelbart att fortsätta för att returnera enTask<int>
till den som ringer, som sedan kan fortsätta med annan bearbetning efter behov. - När
GetByteArrayAsync()
har avslutat nedladdningen kommer den att lösauppgiften
som den returnerade med den nedladdade datan. Detta kommer att utlösa en callback och fåFindPageSizeAsync()
att fortsätta körningen genom att tilldela det värdet tilldata
. - Slutligen returnerar metoden
data.Length
, ett enkelt heltal som anger längden på matrisen. Kompilatorn omtolkar detta som att lösauppgiften
den returnerade tidigare, vilket utlöser ett återuppringning i metodens anropare för att göra något med det längdvärdet.
En funktion som använder async/await kan använda så många await
-uttryck som den vill, och var och en kommer att hanteras på samma sätt (även om ett löfte endast kommer att returneras till den som ringer för första await, medan varannan await kommer att använda interna återuppringningar) . En funktion kan också innehålla ett löftesobjekt direkt och göra annan bearbetning först (inklusive att starta andra asynkrona uppgifter), fördröja i väntan på löftet tills dess resultat behövs. Funktioner med löften har också metoder för löfteaggregering som gör att du kan vänta på flera löften samtidigt eller i något speciellt mönster (som C#:s Task.WhenAll() ,
som returnerar en värdelös Task
som löser sig när alla uppgifterna i argumenten har lösts) . Många löftestyper har också ytterligare funktioner utöver vad asynkron/vänta-mönstret normalt använder, som att kunna ställa in mer än ett resultatuppringning eller inspektera framstegen för en särskilt långvarig uppgift.
I det speciella fallet med C#, och på många andra språk med denna språkfunktion, är async/await-mönstret inte en central del av språkets körtid, utan implementeras istället med lambdas eller fortsättningar vid kompilering . Till exempel skulle C#-kompilatorn sannolikt översätta ovanstående kod till något i stil med följande innan den översattes till dess IL- bytekodformat :
public Task < int > FindPageSizeAsync ( Uri uri ) { var client = new HttpClient (); Task < byte []> dataTask = klient . GetByteArrayAsync ( uri ); Task < int > afterDataTask = dataTask . ContinueWith (( originalTask ) => { return originalTask . Result . Length ; }); returnera efter DataTask ; }
På grund av detta, om en gränssnittsmetod behöver returnera ett löftesobjekt, men själv inte kräver await
i kroppen för att vänta på några asynkrona uppgifter, behöver den inte heller asynkronmodifieraren
och kan istället returnera ett löfteobjekt direkt. En funktion kan till exempel kunna tillhandahålla ett löfte som omedelbart löser sig till ett resultatvärde (som C#:s Task.FromResult()
), eller så kan den helt enkelt returnera en annan metods löfte som råkar vara det exakta löfte som behövs (som när skjuta upp till en överbelastning).
En viktig förbehåll för denna funktionalitet är dock att även om koden liknar traditionell blockeringskod, är koden faktiskt icke-blockerande och potentiellt flertrådad, vilket innebär att många mellanliggande händelser kan inträffa medan man väntar på att löftet som en avvaktning riktar sig mot ska lösas
. Till exempel kan följande kod, även om den alltid lyckas med en blockeringsmodell utan att vänta
, uppleva ingripande händelser under väntan
och kan således hitta delat tillstånd som ändrats under den:
var a = tillstånd . a ; var klient = ny HttpClient (); var data = inväntar klient . GetByteArrayAsync ( uri ); Felsökning . Påstå ( a == tillstånd . a ); // Potentiellt misslyckande, eftersom värdet på state.a kan ha ändrats // av hanteraren av potentiellt ingripande händelse. returnera data . Längd ;
I F#
2007 lade F# till asynkrona arbetsflöden med version 2.0. De asynkrona arbetsflödena implementeras som CE ( beräkningsuttryck) . De kan definieras utan att specificera någon speciell kontext (som asynkron
i C#). F# asynkrona arbetsflöden lägger till en smäll (!) till nyckelord för att starta asynkrona uppgifter.
Följande asynkronfunktion laddar ner data från en URL med hjälp av ett asynkront arbetsflöde:
låt asyncSumPageSizes ( uris : # seq < Uri >) : Async < int > = async { använd httpClient = ny HttpClient () låt! sidor = uris |> Sekv . karta ( httpClient . GetStringAsync >> Async . AwaitTask ) |> Async . Parallella retursidor | > Sekv . fold ( kul ackumulatorström - > ström . Längd + ackumulator ) } 0
I C#
2012 lade C# till async/await-mönstret i C# med version 5.0, som Microsoft refererar till som det uppgiftsbaserade asynkrona mönstret (TAP). Asynkmetoder returnerar vanligtvis antingen void
, Task
, Task<T>
, ValueTask
eller ValueTask<T>
. Användarkod kan definiera anpassade typer som asynkroniseringsmetoder kan returnera genom anpassade asynkmetoder, men detta är ett avancerat och sällsynt scenario. Asynkroniseringsmetoder som returnerar void
är avsedda för händelsehanterare ; i de flesta fall där en synkron metod skulle returnera void
, rekommenderas att returnera Task istället, eftersom det möjliggör en mer intuitiv hantering av undantag.
Metoder som använder await
måste deklareras med nyckelordet async .
I metoder som har ett returvärde av typen Task<T> måste
metoder som deklareras med async
ha en retursats av typen som kan tilldelas T
istället för Task<T>
; kompilatorn lindar värdet i Task<T>
generiskt. Det är också möjligt att avvakta
metoder som har en returtyp av Task
eller Task<T>
som deklareras utan asynkron
.
Följande asynkronmetod laddar ner data från en URL med await
. Eftersom den här metoden utfärdar en uppgift för varje uri innan den kräver komplettering med await
, kan resurserna laddas samtidigt istället för att vänta på att den sista resursen ska avslutas innan nästa laddas.
0
public async Task < int > SumPageSizesAsync ( IEnumerable < Uri > uris ) { var client = new HttpClient (); int totalt = ; var loadUriTasks = ny lista < Task < byte []>>(); foreach ( var uri i uris ) { var loadUriTask = klient . GetByteArrayAsync ( uri ); loadUriTasks . Lägg till ( loadUriTask ); } foreach ( var loadUriTask i loadUriTasks ) { statusText . Text = $"Hittade {total} byte ..." ; var resourceAsBytes = await loadUriTask ; totalt += resourceAsBytes . Längd ; } statusText . Text = $"Hittade {total} byte totalt" ; returnera totalt ; }
I Scala
I den experimentella Scala-async-förlängningen till Scala är await
en "metod", även om den inte fungerar som en vanlig metod. Dessutom, till skillnad från i C# 5.0 där en metod måste markeras som asynkron, i Scala-async, omges ett kodblock av ett asynkront "anrop" .
Hur det fungerar
I Scala-async implementeras async faktiskt med hjälp av ett Scala-makro, vilket gör att
kompilatorn sänder ut annan kod och producerar en ändlig tillståndsmaskinimplementering (som anses vara mer effektiv än en monadisk implementering, men mindre bekväm att skriva för hand ).
Det finns planer på att Scala-async ska stödja en mängd olika implementeringar, inklusive icke-asynkrona.
I Python
Python 3.5 (2015) har lagt till stöd för async/await som beskrivs i PEP 492 (skriven och implementerad av Yury Selivanov).
importera asyncio async def main (): skriv ut ( "hej" ) vänta på asyncio . sömn ( 1 ) print ( "värld" ) asyncio . kör ( huvudsak ())
I JavaScript
Vänta-operatorn i JavaScript kan endast användas inifrån en asynkronfunktion. Om parametern är ett löfte kommer exekveringen av asynkroniseringsfunktionen att återupptas när löftet är löst (såvida inte löftet avvisas, i vilket fall kommer ett fel att skapas som kan hanteras med normal JavaScript-undantagshantering ) . Om parametern inte är ett löfte kommer själva parametern att returneras omedelbart.
Många bibliotek tillhandahåller löftesobjekt som också kan användas med await, så länge de matchar specifikationen för inbyggda JavaScript-löften. Men löften från jQuery -biblioteket var inte Promises/A+-kompatibla förrän jQuery 3.0.
Här är ett exempel (modifierat från den här artikeln):
async -funktion createNewDoc () { låt svar = invänta db . inlägg ({}); // posta ett nytt dokument returnera db . få ( svar . id ); // hitta med id } asynkron funktion main () { try { let doc = await createNewDoc (); konsol . log ( doc ); } fånga ( fel ) { konsol . log ( fel ); } } main ();
Node.js version 8 innehåller ett verktyg som gör det möjligt att använda standardbibliotekets callback-baserade metoder som löften.
I C++
I C++ har await (som heter co_await i C++) officiellt slagits samman till version 20 . Stöd för det, coroutines och nyckelord som co_await
finns i GCC- och MSVC- kompilatorer medan Clang har partiellt stöd.
Det är värt att notera att std::promise och std::future, även om det verkar som att de skulle vara väntade objekt, implementerar inget av de maskiner som krävs för att returneras från coroutines och inväntas med co_await. Programmerare måste implementera ett antal offentliga medlemsfunktioner, såsom await_ready
, await_suspend
och await_resume
på returtypen för att typen ska inväntas. Detaljer finns på cppreference.
0
#include <iostream> #include "CustomAwaitableTask.h" med namnutrymme std ; CustomAwaitableTask < int > add ( int a , int b ) { int c = a + b ; co_return c ; } CustomAwaitableTask < int > test () { int ret = co_await add ( 1 , 2 ); cout << "return" << ret << endl ; co_return ret ; } int main () { auto task = test (); återvända ; }
I C
Det finns inget officiellt stöd för await/async på C-språket ännu. Vissa coroutine-bibliotek som s_task simulerar nyckelorden väntar/asynkroniseras med makron.
0
0
0
#include <stdio.h> #include "s_task.h" // definiera stackminne för uppgifter int g_stack_main [ 64 * 1024 / sizeof ( int )]; int g_stack0 [ 64 * 1024 / sizeof ( int )]; int g_stack1 [ 64 * 1024 / sizeof ( int )]; void sub_task ( __async__ , void * arg ) { int i ; int n = ( int )( storlek_t ) arg ; for ( i = ; i < 5 ; ++ i ) { printf ( "uppgift %d, fördröjningssekunder = %d, i = %d \n " , n , n , i ); s_task_msleep ( __await__ , n * 1000 ); //s_task_yield(__await__); } } void main_task ( __async__ , void * arg ) { int i ; // skapa två underuppgifter s_task_create ( g_stack0 , sizeof ( g_stack0 ), sub_task , ( void * ) 1 ); s_task_create ( g_stack1 , sizeof ( g_stack1 ), sub_task , ( void * ) 2 ); för ( i = ; i < 4 ; ++ i ) { printf ( "task_main arg = %p, i = %d \n " , arg , i ); s_task_yield ( __await__ ); } // vänta på deluppgifterna för att avsluta s_task_join ( __await__ , g_stack0 ); s_task_join ( __await__ , g_stack1 ); } int main ( int argc , char * argv ) { s_task_init_system (); //skapa huvuduppgiften s_task_create ( g_stack_main , sizeof ( g_stack_main ), main_task , ( void * )( size_t ) argc ); s_task_join ( __await__ , g_stack_main ); printf ( "all uppgift är över \n " ); återvända ; }
I Perl 5
The Future::AsyncAwait-modulen var föremål för ett Perl Foundation-anslag i september 2018.
I Rust
Den 7 november 2019 släpptes async/await på den stabila versionen av Rust. Asynkronisera funktioner i Rust desugar till vanliga funktioner som returnerar värden som implementerar Future-egenskapen. För närvarande implementeras de med en finita tillståndsmaskin .
// I lådans Cargo.toml behöver vi `futures = "0.3.0"` i beroendesektionen, // så att vi kan använda futures-backen extern crate futures ; // Det finns ingen executor för närvarande i `std`-biblioteket. // Detta avsockrar till något som // `fn async_add_one(num: u32) -> impl Future<Output = u32>` async fn async_add_one ( num : u32 ) -> u32 { num + 1 } async fn example_task () { let nummer = async_add_one ( 5 ). vänta ; println! ( "5 + 1 = {}" , nummer ); } fn main () { // Att skapa framtiden startar inte körningen. låt framtid = exempel_uppgift (); // `Framtiden` körs bara när vi faktiskt pollar den, till skillnad från Javascript. futures :: executor :: block_on ( framtida ); }
I Swift
Swift 5.5 (2021) lade till stöd för async/await som beskrivs i SE-0296.
func getNumber () async throws -> Int { try await Task . sömn ( nanosekunder : 1_000_000_000 ) retur 42 } Uppgift { låt först = försök avvakta getNumber ( ) låt andra = försök invänta getNumber ( ) print ( första + andra ) }
Fördelar och kritik
En betydande fördel med async/wait-mönstret i språk som stöder det är att asynkron, icke-blockerande kod kan skrivas, med minimal overhead, och ser nästan ut som traditionell synkron, blockerande kod. I synnerhet har det hävdats att avvakta är det bästa sättet att skriva asynkron kod i program som skickar meddelanden ; i synnerhet att vara nära att blockera kod, läsbarhet och den minimala mängden boilerplate-kod angavs som väntande fördelar. Som ett resultat gör async/await det lättare för de flesta programmerare att resonera om sina program, och await tenderar att främja bättre, mer robust icke-blockerande kod i applikationer som kräver det. Sådana applikationer sträcker sig från program som presenterar grafiska användargränssnitt till massivt skalbara tillståndsprogram på serversidan, såsom spel och finansiella applikationer.
När man kritiserar await har det noterats att await tenderar att göra att omgivande kod också är asynkron; å andra sidan har det hävdats att denna smittsamma karaktär av koden (ibland jämförd med ett "zombievirus") är inneboende i alla typer av asynkron programmering, så avvakta som sådan är inte unik i detta avseende.