Händelser
17 mars 23 - 21 mars 23
Gå med i mötesserien för att skapa skalbara AI-lösningar baserat på verkliga användningsfall med andra utvecklare och experter.
Registrera dig nuDen här webbläsaren stöds inte längre.
Uppgradera till Microsoft Edge och dra nytta av de senaste funktionerna och säkerhetsuppdateringarna, samt teknisk support.
Asynkron programmering är en mekanism som är nödvändig för moderna program av olika skäl. Det finns två primära användningsfall som de flesta utvecklare kommer att stöta på:
Även om bakgrundsarbete ofta omfattar användning av flera trådar är det viktigt att tänka på begreppen asynkron och multitrådning separat. I själva verket är de separata problem, och den ena antyder inte den andra. I den här artikeln beskrivs de separata begreppen mer detaljerat.
Föregående punkt – att asynkronitet är oberoende av användningen av flera trådar – är värd att förklara lite längre. Det finns tre begrepp som ibland är relaterade, men strikt oberoende av varandra:
Alla tre är ortoggoniska begrepp, men kan enkelt sammanflätas, särskilt när de används tillsammans. Du kan till exempel behöva köra flera asynkrona beräkningar parallellt. Den här relationen innebär inte att parallellitet eller asynkronitet innebär varandra.
Om du tänker på etymologin för ordet "asynkron" finns det två delar:
När du sätter ihop dessa två termer ser du att "asynkron" betyder "inte samtidigt". Det var allt! Det finns ingen konsekvens av samtidighet eller parallellitet i den här definitionen. Detta gäller även i praktiken.
I praktiken schemaläggs asynkrona beräkningar i F# att köras oberoende av huvudprogrammets flöde. Den här oberoende körningen innebär inte samtidighet eller parallellitet, och det innebär inte heller att en beräkning alltid sker i bakgrunden. I själva verket kan asynkrona beräkningar till och med köras synkront, beroende på beräkningens natur och den miljö som beräkningen körs i.
Det viktigaste du bör ha är att asynkrona beräkningar är oberoende av huvudprogramflödet. Även om det finns få garantier för när eller hur en asynkron beräkning körs finns det vissa metoder för att orkestrera och schemalägga dem. Resten av den här artikeln utforskar grundläggande begrepp för F#-asynkronisering och hur du använder de typer, funktioner och uttryck som är inbyggda i F#.
I F# är asynkron programmering centrerad kring två grundläggande begrepp: asynkrona beräkningar och uppgifter.
Async<'T>
med async { }
uttryck, som representerar en sammansättningsbar asynkron beräkning som kan startas för att bilda en uppgift.Task<'T>
, med task { }
uttryck, som representerar en körande .NET-uppgift.I allmänhet bör du överväga att använda task {…}
över async {…}
i ny kod om du samverkar med .NET-bibliotek som använder uppgifter, och om du inte förlitar dig på asynkrona kod tailcalls eller implicit annulleringstokenspridning.
Du kan se de grundläggande begreppen för "asynkron" programmering i följande exempel:
open System
open System.IO
// Perform an asynchronous read of a file using 'async'
let printTotalFileBytesUsingAsync (path: string) =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
printTotalFileBytesUsingAsync "path-to-file.txt"
|> Async.RunSynchronously
Console.Read() |> ignore
0
I exemplet printTotalFileBytesUsingAsync
är funktionen av typen string -> Async<unit>
. Att anropa funktionen kör inte den asynkrona beräkningen. I stället returneras en Async<unit>
som fungerar som en specifikation av det arbete som ska köras asynkront. Den anropar Async.AwaitTask
i sin brödtext, vilket konverterar resultatet av ReadAllBytesAsync till en lämplig typ.
En annan viktig rad är anropet till Async.RunSynchronously
. Det här är en av startfunktionerna för Async-modulen som du måste anropa om du vill köra en Asynkron F#-beräkning.
Det här är en grundläggande skillnad med programmeringsstilen async
C#/Visual Basic. I F#kan asynkrona beräkningar betraktas som kalla uppgifter. De måste uttryckligen startas för att faktiskt köras. Detta har vissa fördelar eftersom du kan kombinera och sekvensisera asynkront arbete mycket enklare än i C# eller Visual Basic.
Här är ett exempel som bygger på det föregående genom att kombinera beräkningar:
open System
open System.IO
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Parallel
|> Async.Ignore
|> Async.RunSynchronously
0
Som du ser main
har funktionen en hel del fler element. Konceptuellt gör den följande:
Async<unit>
beräkningar med Seq.map
.Async<'T[]>
som schemalägger och kör printTotalFileBytes
beräkningen parallellt när den körs.Async<unit>
som kör parallellberäkningen och ignorera dess resultat (som är en unit[]
).Async.RunSynchronously
och blockera tills den är klar.När det här programmet körs printTotalFileBytes
körs parallellt för varje kommandoradsargument. Eftersom asynkrona beräkningar körs oberoende av programflödet finns det ingen definierad ordning där de skriver ut sin information och slutför körningen. Beräkningarna schemaläggs parallellt, men deras körningsordning garanteras inte.
Eftersom Async<'T>
är en arbetsspecifikation snarare än en uppgift som redan körs kan du enkelt utföra mer invecklade omvandlingar. Här är ett exempel som sekvenser en uppsättning Async-beräkningar så att de körs en efter en.
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Sequential
|> Async.Ignore
|> Async.RunSynchronously
|> ignore
Detta schemalägger printTotalFileBytes
att köras i ordning på elementen argv
i stället för att schemalägga dem parallellt. Eftersom varje efterföljande åtgärd inte schemaläggs förrän den föregående beräkningen har slutförts, sekvenseras beräkningen så att det inte finns någon överlappning i deras körning.
När du skriver asynkron kod i F# interagerar du vanligtvis med ett ramverk som hanterar schemaläggning av beräkningar åt dig. Detta är dock inte alltid fallet, så det är bra att förstå de olika funktioner som kan användas för att schemalägga asynkront arbete.
Eftersom Asynkrona F#-beräkningar är en arbetsspecifikation i stället för en representation av arbete som redan körs, måste de uttryckligen startas med en startfunktion. Det finns många Async-startmetoder som är användbara i olika kontexter. I följande avsnitt beskrivs några av de vanligaste startfunktionerna.
Startar en underordnad beräkning i en asynkron beräkning. Detta gör att flera asynkrona beräkningar kan köras samtidigt. Den underordnade beräkningen delar en annulleringstoken med den överordnade beräkningen. Om den överordnade beräkningen avbryts avbryts även den underordnade beräkningen.
Signatur:
computation: Async<'T> * ?millisecondsTimeout: int -> Async<Async<'T>>
Används för att:
Vad du bör se upp för:
Async.StartChild
är inte detsamma som att schemalägga dem parallellt. Om du vill schemalägga beräkningar parallellt använder du Async.Parallel
.Kör en asynkron beräkning med början omedelbart på den aktuella operativsystemtråden. Det här är användbart om du behöver uppdatera något i den anropande tråden under beräkningen. Om en asynkron beräkning till exempel måste uppdatera ett användargränssnitt (till exempel uppdatera ett förloppsfält) ska det Async.StartImmediate
användas.
Signatur:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Används för att:
Vad du bör se upp för:
Async.StartImmediate
är sannolikt olämpligt att använda.Kör en beräkning i trådpoolen. Returnerar ett Task<TResult> som kommer att slutföras i motsvarande tillstånd när beräkningen avslutas (ger resultatet, utlöser undantag eller avbryts). Om ingen annulleringstoken anges används standardtoken för annullering.
Signatur:
computation: Async<'T> * ?taskCreationOptions: TaskCreationOptions * ?cancellationToken: CancellationToken -> Task<'T>
Används för att:
Vad du bör se upp för:
Task
objekt, vilket kan öka kostnaderna om det används ofta.Schemalägger en sekvens med asynkrona beräkningar som ska köras parallellt, vilket ger en matris med resultat i den ordning de angavs. Graden av parallellitet kan justeras/begränsas genom att ange parametern maxDegreeOfParallelism
.
Signatur:
computations: seq<Async<'T>> * ?maxDegreeOfParallelism: int -> Async<'T[]>
När du ska använda den:
Vad du bör se upp för:
Schemalägger en sekvens med asynkrona beräkningar som ska köras i den ordning som de skickas. Den första beräkningen körs, sedan nästa och så vidare. Inga beräkningar körs parallellt.
Signatur:
computations: seq<Async<'T>> -> Async<'T[]>
När du ska använda den:
Vad du bör se upp för:
Returnerar en asynkron beräkning som väntar på att den angivna Task<TResult> ska slutföras och returnerar resultatet som en Async<'T>
Signatur:
task: Task<'T> -> Async<'T>
Används för att:
Vad du bör se upp för:
Skapar en asynkron beräkning som kör en viss Async<'T>
, returnerar en Async<Choice<'T, exn>>
. Om den angivna Async<'T>
slutförs returneras en Choice1Of2
med det resulterande värdet. Om ett undantag utlöses innan det slutförs returneras ett Choice2of2
med det upphöjda undantaget. Om den används i en asynkron beräkning som i sig består av många beräkningar, och en av dessa beräkningar genererar ett undantag, stoppas den omfattande beräkningen helt.
Signatur:
computation: Async<'T> -> Async<Choice<'T, exn>>
Används för att:
Vad du bör se upp för:
Skapar en asynkron beräkning som kör den angivna beräkningen men släpper resultatet.
Signatur:
computation: Async<'T> -> Async<unit>
Används för att:
ignore
funktionen för icke-asynkron kod.Vad du bör se upp för:
Async.Ignore
för att du vill använda Async.Start
eller en annan funktion som kräver Async<unit>
kan du överväga om det är okej att ta bort resultatet. Undvik att ignorera resultat bara för att passa en typsignatur.Kör en asynkron beräkning och väntar på resultatet i den anropande tråden. Sprider ett undantag om beräkningen ger ett. Det här anropet blockerar.
Signatur:
computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T
När du ska använda den:
Vad du bör se upp för:
Async.RunSynchronously
blockerar den anropande tråden tills körningen har slutförts.Startar en asynkron beräkning som returneras unit
i trådpoolen. Väntar inte på att den ska slutföras och/eller observerar ett undantagsresultat. Kapslade beräkningar som startas med Async.Start
startas oberoende av den överordnade beräkningen som anropade dem. Deras livslängd är inte kopplad till någon överordnad beräkning. Om den överordnade beräkningen avbryts avbryts inga underordnade beräkningar.
Signatur:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Använd endast när:
Vad du bör se upp för:
Async.Start
sprids inte till anroparen. Anropsstacken kommer att vara helt oansluten.printfn
) som startas med Async.Start
leder inte till att effekten inträffar på huvudtråden i ett programs körning.Om du använder async { }
programmering kan du behöva samverka med ett .NET-bibliotek eller en C#-kodbas som använder asynkron asynkron programmering i asynkron asynkron programmering. Eftersom C# och majoriteten av .NET-biblioteken använder typerna Task<TResult> och Task som sina kärnabstraktioner kan detta ändra hur du skriver din Asynkrona F#-kod.
Ett alternativ är att växla till att skriva .NET-uppgifter direkt med hjälp av task { }
. Du kan också använda Async.AwaitTask
funktionen för att invänta en .NET-asynkron beräkning:
let getValueFromLibrary param =
async {
let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
return value
}
Du kan använda Async.StartAsTask
funktionen för att skicka en asynkron beräkning till en .NET-anropare:
let computationForCaller param =
async {
let! result = getAsyncResult param
return result
} |> Async.StartAsTask
Om du vill arbeta med API:er som använder Task (dvs. .NET async-beräkningar som inte returnerar ett värde) kan du behöva lägga till ytterligare en funktion som konverterar en Async<'T>
till en Task:
module Async =
// Async<unit> -> Task
let startTaskFromAsyncUnit (comp: Async<unit>) =
Async.StartAsTask comp :> Task
Det finns redan en Async.AwaitTask
som accepterar en Task som indata. Med den här och den tidigare definierade startTaskFromAsyncUnit
funktionen kan du starta och invänta Task typer från en F#-asynkron beräkning.
I F# kan du skriva uppgifter direkt med hjälp av task { }
, till exempel:
open System
open System.IO
/// Perform an asynchronous read of a file using 'task'
let printTotalFileBytesUsingTasks (path: string) =
task {
let! bytes = File.ReadAllBytesAsync(path)
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
let task = printTotalFileBytesUsingTasks "path-to-file.txt"
task.Wait()
Console.Read() |> ignore
0
I exemplet printTotalFileBytesUsingTasks
är funktionen av typen string -> Task<unit>
. Om du anropar funktionen startas aktiviteten.
Anropet väntar tills task.Wait()
uppgiften har slutförts.
Även om trådning nämns i hela den här artikeln finns det två viktiga saker att komma ihåg:
En beräkning kan till exempel faktiskt köras på anroparens tråd, beroende på arbetets natur. En beräkning kan också "hoppa" mellan trådar och låna dem under en liten tid för att göra användbart arbete mellan perioder av "väntar" (till exempel när ett nätverksanrop är under överföring).
Även om F# ger vissa möjligheter att starta en asynkron beräkning på den aktuella tråden (eller uttryckligen inte på den aktuella tråden), är asynkron i allmänhet inte associerad med en viss trådstrategi.
Feedback om .NET
.NET är ett öppen källkod projekt. Välj en länk för att ge feedback:
Händelser
17 mars 23 - 21 mars 23
Gå med i mötesserien för att skapa skalbara AI-lösningar baserat på verkliga användningsfall med andra utvecklare och experter.
Registrera dig nuUtbildning
Utbildningsväg
Ta dina första steg med F# - Training
F# är ett plattformsoberoende programmeringsspråk med öppen källkod som gör det enkelt att skriva kortfattad, högpresterande, robust och praktisk kod. Det är ett allmänt språk som gör att du kan skapa många olika typer av program som Webb-API, Desktop, IoT, Gaming med mera.
Dokumentation
Lär dig mer om stöd i programmeringsspråket F# för att skriva asynkrona uttryck, som körs utan att blockera körning av annat arbete.
Lär dig mer om stöd i programmeringsspråket F# för att skriva uppgiftsuttryck, som skapar .NET-uppgifter direkt.
Lär dig hur du skapar praktisk syntax för att skriva beräkningar i F# som kan sekvenseras och kombineras med hjälp av kontrollflödeskonstruktioner och bindningar.