Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Artikel ini menjelaskan dukungan di F# untuk ekspresi tugas, yang mirip dengan ekspresi asinkron tetapi memungkinkan Anda menulis tugas .NET secara langsung. Seperti ekspresi asinkron, ekspresi tugas menjalankan kode secara asinkron, yaitu, tanpa memblokir eksekusi pekerjaan lain.
Kode asinkron biasanya ditulis menggunakan ekspresi asinkron. Menggunakan ekspresi tugas lebih disukai saat mengoperasikan secara ekstensif dengan pustaka .NET yang membuat atau menggunakan tugas .NET. Ekspresi tugas juga dapat meningkatkan performa dan pengalaman pemecahan masalah. Namun, ekspresi tugas dilengkapi dengan beberapa batasan, yang dijelaskan nanti dalam artikel.
Sintaksis
task { expression }
Dalam sintaks sebelumnya, komputasi yang diwakili oleh expression disiapkan untuk dijalankan sebagai tugas .NET. Tugas dimulai segera setelah kode ini dijalankan dan berjalan pada utas saat ini sampai operasi asinkron pertamanya dilakukan (misalnya, tidur asinkron, I/O asinkron, atau operasi asinkron primitif lainnya). Jenis ekspresi adalah Task<'T>, di mana 'T adalah jenis yang dikembalikan oleh ekspresi saat return kata kunci digunakan.
Mengikat dengan menggunakan fungsi let!
Dalam ekspresi tugas, beberapa ekspresi dan operasi sinkron, dan beberapa bersifat asinkron. Ketika Anda menunggu hasil operasi asinkron, alih-alih pengikatan biasa let , Anda menggunakan let!. Efek dari let! adalah memungkinkan eksekusi untuk berlanjut pada komputasi atau utas lain saat komputasi sedang dilakukan. Setelah sisi kanan pengikatan let! kembali, sisa tugas melanjutkan eksekusi.
Kode berikut menunjukkan perbedaan antara let dan let!. Baris kode yang menggunakan let hanya membuat tugas sebagai objek yang dapat Anda tunggu nanti dengan menggunakan, misalnya, task.Wait() atau task.Result. Baris kode yang menggunakan let! memulai tugas dan menunggu hasilnya.
// let just stores the result as a task.
let (result1 : Task<int>) = stream.ReadAsync(buffer, offset, count, cancellationToken)
// let! completes the asynchronous operation and returns the data.
let! (result2 : int) = stream.ReadAsync(buffer, offset, count, cancellationToken)
Ekspresi F# task { } dapat menunggu jenis operasi asinkron berikut:
- Tugas .NET, Task<TResult> dan Task non-generik.
- Tugas nilai .NET, ValueTask<TResult> dan ValueTask yang non-generik.
- F# asinkron komputasi
Async<T>. - Objek apa pun yang mengikuti pola "GetAwaiter" yang ditentukan dalam F# RFC FS-1097.
ekspresi return
Dalam ekspresi tugas, return expr digunakan untuk mengembalikan hasil tugas.
ekspresi return!
Dalam ekspresi tugas, return! expr digunakan untuk mengembalikan hasil tugas lain. Ini setara dengan menggunakan let! dan kemudian segera mengembalikan hasilnya.
Alur kontrol
Ekspresi tugas dapat menyertakan konstruksi for .. in .. do alur kontrol, while .. do, try .. with .., try .. finally .., if .. then .. else, dan if .. then ... Ini pada gilirannya dapat mencakup konstruksi tugas lebih lanjut, kecuali untuk with dan finally handler, yang dijalankan secara sinkron. Jika Anda memerlukan proses asinkron try .. finally .., gunakan pengikatan use dalam kombinasi dengan objek dari tipe IAsyncDisposable.
use dan use! pengikatan
Dalam ekspresi tugas, use pengikatan dapat mengikat ke nilai jenis IDisposable atau IAsyncDisposable. Untuk yang disebutkan terakhir, operasi pembersihan limbah dilakukan secara asinkron.
Selain let!, Anda dapat menggunakan use! untuk melakukan pengikatan asinkron. Perbedaan antara let! dan use! sama dengan perbedaan antara let dan use. Untuk use!, objek dibuang pada penutupan cakupan saat ini. Perhatikan bahwa di F# 6, use! tidak memungkinkan nilai diinisialisasi menjadi null, meskipun use demikian.
open System
open System.IO
open System.Security.Cryptography
task {
// use IDisposable
use httpClient = new Net.Http.HttpClient()
// use! Task<IDisposable>
use! exampleDomain = httpClient.GetAsync "https://example.com/data.enc"
// use IDisposable
use aes = Aes.Create()
aes.KeySize <- 256
aes.GenerateIV()
aes.GenerateKey()
// do! Task
do! File.WriteAllTextAsync("key.iv.txt", $"Key: {Convert.ToBase64String aes.Key}\nIV: {Convert.ToBase64String aes.IV}")
// use IAsyncDisposable
use outputStream = File.Create "secret.enc"
// use IDisposable
use encryptor = aes.CreateEncryptor()
// use IAsyncDisposable
use cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)
// do! Task
do! exampleDomain.Content.CopyToAsync cryptoStream
}
Tugas Bernilai
Tugas nilai adalah struktur yang digunakan untuk menghindari alokasi dalam pemrograman berbasis tugas. Tugas nilai adalah nilai sementara yang berubah menjadi tugas nyata dengan menggunakan .AsTask().
Untuk membuat tugas nilai dari ekspresi tugas, gunakan |> ValueTask<ReturnType> atau |> ValueTask. Contohnya:
let makeTask() =
task { return 1 }
makeTask() |> ValueTask<int>
and! pengikatan (mulai dari F# 10)
Dalam ekspresi tugas, dimungkinkan untuk secara bersamaan menunggu beberapa operasi asinkron (Task<'T>, ValueTask<'T>, Async<'T> dll). Membandingkan:
// We'll wait for x to resolve and then for y to resolve. Overall execution time is sum of two execution times.
let getResultsSequentially() =
task {
let! x = getX()
let! y = getY()
return x, y
}
// x and y will be awaited concurrently. Overall execution time is the time of the slowest operation.
let getResultsConcurrently() =
task {
let! x = getX()
and! y = getY()
return x, y
}
Menambahkan token pembatalan dan pemeriksaan pembatalan
Tidak seperti ekspresi asinkron F#, ekspresi tugas tidak secara implisit meneruskan token pembatalan dan tidak secara implisit melakukan pemeriksaan pembatalan. Jika kode Anda memerlukan token pembatalan, Anda harus menentukan token pembatalan sebagai parameter. Contohnya:
open System.Threading
let someTaskCode (cancellationToken: CancellationToken) =
task {
cancellationToken.ThrowIfCancellationRequested()
printfn $"continuing..."
}
Jika Anda berniat untuk membatalkan kode dengan benar, periksa dengan cermat apakah Anda meneruskan token pembatalan ke semua operasi pustaka .NET yang mendukung pembatalan. Misalnya, Stream.ReadAsync memiliki beberapa kelebihan beban, salah satunya menerima token pembatalan. Jika Anda tidak menggunakan kelebihan beban ini, operasi baca asinkron tertentu tidak akan dapat dibatalkan.
Tugas latar belakang
Secara default, tugas .NET dijadwalkan menggunakan SynchronizationContext.Current jika ada. Ini memungkinkan tugas untuk berfungsi sebagai agen yang kooperatif dan saling terkait yang dijalankan pada utas antarmuka pengguna tanpa memblokir UI. Jika tidak ada, kelanjutan tugas dijadwalkan ke kumpulan utas .NET.
Dalam praktiknya, sering kali diinginkan bahwa kode pustaka yang menghasilkan tugas mengabaikan konteks sinkronisasi dan sebaliknya selalu beralih ke kumpulan utas .NET, jika perlu. Anda dapat mencapai hal ini menggunakan backgroundTask { }:
backgroundTask { expression }
Tugas latar belakang mengabaikan segala SynchronizationContext.Current dalam arti berikut: jika dimulai pada utas yang memiliki SynchronizationContext.Current dengan nilai tidak-null, tugas beralih ke utas latar belakang di kumpulan utas menggunakan Task.Run. Jika dimulai pada utas dengan null SynchronizationContext.Current, eksekusi dilakukan pada utas yang sama.
Nota
Dalam praktiknya, ini berarti bahwa panggilan ke ConfigureAwait(false) biasanya tidak diperlukan dalam kode tugas F#. Sebagai gantinya, tugas yang dimaksudkan untuk dijalankan di latar belakang harus ditulis menggunakan backgroundTask { ... }. Setiap tugas luar yang mengikat ke tugas latar belakang akan disinkronkan ulang ke SynchronizationContext.Current setelah penyelesaian tugas latar belakang.
Batasan tugas mengenai panggilan ekor
Tidak seperti ekspresi asinkron F#, ekspresi tugas tidak mendukung tailcalls. Artinya, ketika return! dijalankan, tugas saat ini terdaftar sebagai menunggu tugas yang hasilnya dikembalikan. Ini berarti bahwa fungsi dan metode rekursif yang diterapkan menggunakan ekspresi tugas dapat membuat rantai tugas yang jumlahnya tidak terbatas, serta dapat menggunakan tumpukan memori atau area heap yang tidak terbatas. Misalnya, pertimbangkan kode berikut:
let rec taskLoopBad (count: int) : Task<string> =
task {
if count = 0 then
return "done!"
else
printfn $"looping..., count = {count}"
return! taskLoopBad (count-1)
}
let t = taskLoopBad 10000000
t.Wait()
Gaya penulisan kode ini tidak boleh digunakan dengan ekspresi tugas—gaya ini akan membuat serangkaian 10000000 tugas dan menyebabkan StackOverflowException. Jika operasi asinkron ditambahkan pada setiap pemanggilan perulangan, kode akan menggunakan tumpukan yang pada dasarnya tidak terbatas. Pertimbangkan untuk mengalihkan kode ini untuk menggunakan perulangan eksplisit, misalnya:
let taskLoopGood (count: int) : Task<string> =
task {
for i in count .. 1 do
printfn $"looping... count = {count}"
return "done!"
}
let t = taskLoopGood 10000000
t.Wait()
Jika tailcall asinkron diperlukan, gunakan ekspresi asinkron dalam F#, yang memang mendukung tailcall. Contohnya:
let rec asyncLoopGood (count: int) =
async {
if count = 0 then
return "done!"
else
printfn $"looping..., count = {count}"
return! asyncLoopGood (count-1)
}
let t = asyncLoopGood 1000000 |> Async.StartAsTask
t.Wait()
Implementasi tugas
Tugas diimplementasikan menggunakan Kode yang Dapat Dilanjutkan, fitur baru di F# 6. Tugas dikompilasi ke dalam "Mesin Status yang Bisa Dilanjutkan" oleh pengkompilasi F#. Ini dijelaskan secara rinci dalam RFC kode yang dapat diulang, dan dalam sesi komunitas kompilator F#.