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 antal await- 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 typen int .
  • 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 en Task<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, oavvisad Task<byte[]> till den här funktionen.
  • Med await nyckelordet kopplat till uppgiften kommer denna funktion omedelbart att fortsätta för att returnera en Task<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ösa uppgiften 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 till data .
  • Slutligen returnerar metoden data.Length , ett enkelt heltal som anger längden på matrisen. Kompilatorn omtolkar detta som att lösa uppgiften 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  .  (  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.

Se även