Asinkroni di ekosistem .NET modern bertumpu pada tipe Task dan Task<'T>, dan F# dapat memproduksi sekaligus mengonsumsinya secara langsung. Topik ini membahas workflow task { }, cara Task dari F# ditunggu dari C# dan sebaliknya, konversi dua arah dengan Async.StartAsTask dan Async.AwaitTask, serta pertimbangan memilih antara task dan async. Dengan kedua bentuk workflow tersebut, batas interoperabilitas antara kode F# dan C# yang bersifat asinkron dapat dirancang tanpa lapisan adaptor.
Task sebagai bahasa bersama .NET#
Tipe Task<'T> mewakili satu pekerjaan asinkron yang hasilnya tersedia di kemudian waktu, beserta status berjalan, selesai, gagal, atau dibatalkan. C# menjadikan tipe ini dasar kata kunci async dan await, sedangkan API modern .NET seperti HttpClient dan API berkas mengembalikan Task pada hampir semua operasinya. Karena satu tipe yang sama digunakan lintas bahasa, fungsi F# yang menghasilkan Task dapat ditunggu dengan await di C# tanpa pembungkus, dan fungsi C# yang menghasilkan Task dapat dipanggil dari F# secara langsung. Pada tim yang mencampur kedua bahasa dalam satu solusi, Task menjadi batas interoperabilitas yang wajar karena pemuatan nilainya sudah diperiksa oleh compiler kedua bahasa, tanpa pustaka penghubung apa pun. Status setiap pekerjaan juga terbaca melalui properti Status, sehingga pemeriksaan hasil tidak selalu berupa penantian penuh.
Workflow task di F##
Workflow task { } tersedia sejak F# 6 dan membentuk nilai Task<'T> secara langsung tanpa konversi. Berbeda dengan workflow async yang belum berjalan saat dibentuk, task mulai dieksekusi begitu ekspresinya dievaluasi, lalu eksekusinya berhenti sementara di setiap let! atau do! yang menunggu. Berkas berikut membuktikan kedua sifat itu melalui urutan cetakannya, sekaligus memperlihatkan bahwa hasilnya adalah Task sungguhan yang memiliki metode Wait dan properti Status milik .NET.
// task { } menghasilkan Task<'T> yang mulai berjalan begitu ekspresinya dievaluasi.
open System.Threading.Tasks
let rekapTotal (nilai: int list) : Task<int> =
task {
printfn "pekerjaan task berjalan"
do! Task.Delay 100
return List.sum nilai
}
printfn "sebelum membuat task"
let tugas = rekapTotal [ 10; 20; 30; 40 ]
printfn "sesudah membuat task"
tugas.Wait()
printfn "status akhir: %O" tugas.Status
let total = Async.RunSynchronously (Async.AwaitTask tugas)
printfn "total: %d" totalsebelum membuat task
pekerjaan task berjalan
sesudah membuat task
status akhir: RanToCompletion
total: 100Baris pekerjaan task berjalan tercetak sebelum sesudah membuat task karena badan task mulai berjalan pada saat rekapTotal [ 10; 20; 30; 40 ] dievaluasi. Properti Status menampilkan RanToCompletion, yaitu status standar .NET untuk task yang selesai tanpa error. Pengambilan hasilnya dilakukan dengan Async.AwaitTask, yang mengubah Task menjadi workflow Async agar dapat dijalankan dengan Async.RunSynchronously di dalam skrip. Sifat segera berjalan ini menyerupai pemanggilan fungsi biasa, sehingga tidak ada pemulai terpisah yang diperlukan. Konsekuensinya, task yang dibuat di dalam fungsi sudah bergerak meskipun belum ada pihak yang menunggu hasilnya, dan exception yang dilempar badannya tersimpan pada task tersebut sampai hasilnya ditunggu.
Konsumsi Task dua arah#
Arah pertama adalah F# yang memanggil pustaka .NET atau C#, dan alatnya menyesuaikan workflow yang sedang ditulis. Di dalam task { }, hasil Task diikat dengan let! sebagaimana workflow Async lain, misalnya let! isi = httpClient.GetStringAsync url pada kode yang menggunakan jaringan. Di dalam async { }, Task tidak dapat diikat langsung karena buildernya hanya menerima Async<'T>, sehingga konversinya dilakukan dengan Async.AwaitTask. Arah kedua adalah C# yang memanggil F# apabila fungsi F# dinyatakan bertipe kembalian Task<'T>, karena pemanggil C# menunggunya dengan await sebagaimana fungsi C# lain. Kesalahan yang sering terjadi adalah mengikat Task secara langsung di dalam async { }, dan compiler menolaknya dengan pesan ketidakcocokan tipe sebelum program dijalankan.
Penyesuaian ini hanya terjadi pada batas antara dua tipe workflow. Di dalam task { }, buildernya menerima workflow Async<'T> secara langsung pada let!, sehingga pustaka lama berbasis Async tetap dapat dipanggil dari kode task tanpa konversi manual. Arah sebaliknya membutuhkan Async.AwaitTask, sehingga aturan praktisnya sederhana, yaitu Task digunakan langsung di dalam task dan ditunggu melalui konversi di dalam async.
Konversi antara Async dan Task#
Konversi dari Async ke Task dilakukan dengan Async.StartAsTask, yang menjalankan workflow di thread pool dan mengembalikan Task<'T> yang dapat dibagikan ke pemanggil mana pun. Konversi sebaliknya menggunakan Async.AwaitTask. Berkas berikut mengubah satu workflow Async menjadi Task, mengambil hasilnya melalui properti Result, lalu menunggu Task yang sama sebagai workflow Async.
// Konversi dua arah antara workflow Async dan Task.
open System.Threading.Tasks
let kerjaAsync =
async {
do! Async.Sleep 100
return 7 * 6
}
let sebagaiTask : Task<int> = Async.StartAsTask kerjaAsync
printfn "melalui Task: %d" sebagaiTask.Result
let kembaliKeAsync = Async.AwaitTask sebagaiTask
printfn "melalui Async: %d" (Async.RunSynchronously kembaliKeAsync)melalui Task: 42
melalui Async: 42Kedua jalur memberikan hasil yang sama karena keduanya menunggu pekerjaan yang sama. Perbedaannya terletak pada pihak yang menunggu, yaitu pemanggil .NET umum pada jalur Task dan kode F# pada jalur Async. Konversi ini bersifat satu kali untuk satu workflow, sehingga workflow yang sudah dikonversi menjadi Task sebaiknya ditunggu dalam bentuk Task itu juga, bukan dikonversi bolak-balik. Konversi tidak menyalin pekerjaannya, karena kedua tipe berbagi satu eksekusi yang sama dan hanya berbeda bentuk antarmukanya, sedangkan Async.StartAsTask menerima token pembatalan opsional untuk diteruskan ke workflow di baliknya. Nama kedua fungsi itu juga memperlihatkan arah konversinya, yaitu memulai dari Async dan menghasilkan Task pada yang pertama, serta menunggu Task pada yang kedua.
Memilih antara task dan async#
Kedua workflow berjalan di atas thread pool yang sama, sehingga pilihannya tidak menyangkut performa secara besar, melainkan bentuk nilai yang dilepas ke pemanggil. task { } dikompilasi menjadi mesin status (state machine) .NET yang setara dengan kode C# async, sedangkan async { } menghasilkan objek workflow F# dengan token pembatalan yang mengalir otomatis ke seluruh bagian badannya. Tabel berikut merangkum pertimbangan pemilihannya.
| Aspek | task { } | async { } |
|---|---|---|
| Tipe hasil | Task<'T> | Async<'T> |
| Tersedia sejak | F# 6 | F# sejak versi awal |
| Waktu mulai | segera saat dievaluasi | setelah fungsi pemulai dipanggil |
| Konsumsi dari C# | langsung dengan await | perlu Async.StartAsTask |
| Komposisi kumpulan | Task.WhenAll | Async.Parallel |
| Cocok untuk | API pustaka dan pemanggil lintas bahasa | pipeline internal F# |
Aturan praktisnya mengikuti pihak yang memanggil. Fungsi yang keluar dari batas pustaka, misalnya dipanggil dari proyek C# atau framework .NET, lebih aman bertipe Task<'T>. Fungsi yang hanya bergerak di dalam kode F#, termasuk skrip dan pipeline pengolahan data, bebas memilih async karena seluruh pemanggilnya memahami tipe tersebut.
Ringkasan#
Task<'T>adalah tipe pekerjaan asinkron bersama seluruh bahasa .NET, sehingga menjadi batas interoperabilitas yang wajar antara F# dan C#.task { }membentukTask<'T>yang mulai berjalan saat dievaluasi dan dapat ditunggu dari C# denganawait, sedangkanasync { }menghasilkanAsync<'T>yang dimulai secara eksplisit.- Konversi dua arah menggunakan
Async.StartAsTaskdanAsync.AwaitTask, sedangkan pemilihan antarataskdanasyncmengikuti pihak yang memanggil.
Referensi#
- Task expressions — Microsoft Learn
— dokumentasi workflow
task { }beserta penggunaannya di F#. - Async expressions — Microsoft Learn — dokumentasi workflow async yang menjadi lawan konversi di topik ini.
- Panduan F# — Microsoft Learn — halaman induk dokumentasi resmi F# di .NET.