Interop Task

Asinkroni berbasis Task yang dibagikan langsung dengan C# dan pustaka .NET modern.

Daftar isi

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.

fsharp rekap-task.fsx
// 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" total
text Keluaran
sebelum membuat task
pekerjaan task berjalan
sesudah membuat task
status akhir: RanToCompletion
total: 100

Baris 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.

fsharp konversi-async-task.fsx
// 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)
text Keluaran
melalui Task: 42
melalui Async: 42

Kedua 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.

Aspektask { }async { }
Tipe hasilTask<'T>Async<'T>
Tersedia sejakF# 6F# sejak versi awal
Waktu mulaisegera saat dievaluasisetelah fungsi pemulai dipanggil
Konsumsi dari C#langsung dengan awaitperlu Async.StartAsTask
Komposisi kumpulanTask.WhenAllAsync.Parallel
Cocok untukAPI pustaka dan pemanggil lintas bahasapipeline 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 { } membentuk Task<'T> yang mulai berjalan saat dievaluasi dan dapat ditunggu dari C# dengan await, sedangkan async { } menghasilkan Async<'T> yang dimulai secara eksplisit.
  • Konversi dua arah menggunakan Async.StartAsTask dan Async.AwaitTask, sedangkan pemilihan antara task dan async mengikuti pihak yang memanggil.

Referensi#