Workflow async

I/O non-blocking yang ditulis dengan gaya computation expression dan dijalankan secara efisien.

Daftar isi

Workflow async menyatakan operasi yang perlu menunggu, seperti permintaan jaringan atau pembacaan berkas, tanpa memblokir thread yang sedang berjalan. Topik ini membahas pembentukan workflow async dengan async { }, pengikatan hasil dengan let! dan do!, cara memulai workflow dari luar, serta pengumpulan hasil berurutan dan paralel dengan Async.Parallel. Di bagian akhir, model eksekusinya di atas thread pool dijelaskan agar perilaku efisiensinya terbaca dari kodenya. Contohnya menggunakan jeda simulasi dengan Async.Sleep sehingga seluruhnya dapat dijalankan tanpa jaringan nyata.

Workflow async sebagai nilai#

Ekspresi async { } adalah computation expression, yaitu konstruksi bahasa yang mengubah satu blok kode menjadi satu nilai bertipe Async<'T>. Nilai itu merupakan deskripsi pekerjaan, bukan pekerjaan yang sedang berjalan, sehingga tidak ada baris di dalam badannya yang dieksekusi sebelum workflow dimulai secara eksplisit. Sifat menunda ini terlihat pada potongan berikut, yang mencetak pesan sebelum dan sesudah workflow dibentuk.

fsharp Contoh: workflow async sebagai nilai yang belum berjalan
// Workflow async adalah nilai: tidak berjalan sebelum dimulai.
let kerja =
    async {
        printfn "workflow berjalan"
        return 2 + 3
    }

printfn "workflow sudah dibuat, belum dijalankan"

let hasil = Async.RunSynchronously kerja

printfn "hasil: %d" hasil
text Keluaran
workflow sudah dibuat, belum dijalankan
workflow berjalan
hasil: 5

Keluaran menunjukkan urutan yang berbeda dari urutan penulisan. Baris workflow sudah dibuat, belum dijalankan tercetak lebih dulu karena pembentukan nilai kerja tidak menjalankan badannya. Badan workflow baru berjalan ketika Async.RunSynchronously dipanggil, sehingga baris workflow berjalan menyusul, dan hasil akhirnya diikat pada hasil.

Dengan sifat ini, satu workflow dapat disimpan, dikirim sebagai parameter fungsi, dan dirangkai dengan workflow lain sebelum semuanya dimulai. Pola tunda ini juga membuat satu deskripsi workflow dapat dimulai lebih dari satu kali, karena setiap pemulaian menghasilkan eksekusi baru atas deskripsi yang sama. Tipe hasilnya juga mengikuti isi workflow, sehingga deskripsi yang menghasilkan string dan deskripsi yang menghasilkan bilangan memiliki tipe yang berbeda.

let! dan do! di dalam workflow#

Di dalam workflow, let! menunggu workflow lain selesai lalu mengikat hasilnya pada pengenal, sebagaimana let biasa tetapi untuk nilai Async<'T>. Bentuk do! digunakan ketika workflow lain tidak menghasilkan nilai yang perlu diikat. Async.Sleep memberikan jeda secara non-blocking, yaitu thread yang sedang digunakan dikembalikan selama jeda berlangsung dan digunakan kembali setelah jeda selesai. Berkas berikut merangkai tiga pengambilan catatan yang waktunya disimulasikan dengan Async.Sleep 100, masing-masing 100 milidetik, tanpa jaringan nyata.

fsharp muat-berurutan.fsx
// Tiga pengambilan catatan dijalankan satu per satu dalam satu workflow.
let muatCatatan id =
    async {
        do! Async.Sleep 100
        return sprintf "catatan %d terbaca" id
    }

let berurutan =
    async {
        let! a = muatCatatan 1
        let! b = muatCatatan 2
        let! c = muatCatatan 3
        return [ a; b; c ]
    }

Async.RunSynchronously berurutan
|> List.iter (printfn "%s")
text Keluaran
catatan 1 terbaca
catatan 2 terbaca
catatan 3 terbaca

Keluaran menampilkan tiga catatan sesuai urutan let! karena setiap pengikatan menunggu workflow sebelumnya selesai. Total waktunya adalah jumlah ketiganya, sekitar 300 milidetik, dan selama jeda itu tidak ada thread yang tertahan. Angka simulasi dipilih kecil agar skrip berhenti cepat, sedangkan polanya sama dengan permintaan jaringan sungguhan yang waktunya ditentukan oleh pihak luar. Pengikatan let! hanya menerima nilai Async<'T>, sehingga workflow yang dikomposisi saling pas secara tipe. Apabila satu langkah melempar exception, workflow gabungan berhenti pada langkah tersebut dan Async.RunSynchronously melemparkannya kembali kepada pemanggil.

Memulai workflow dari luar#

Workflow yang sudah dibentuk dimulai dengan fungsi pada modul Async, dan pilihannya menentukan cara hasilnya kembali. Async.RunSynchronously menjalankan workflow dan menahan thread pemanggil sampai hasilnya siap, sehingga cocok untuk titik masuk program dan skrip. Async.Start menjalankan workflow di thread pool tanpa mengembalikan hasil ke pemanggilnya. Async.StartWithContinuations menerima tiga fungsi lanjutan (continuation), yaitu fungsi yang dipanggil ketika workflow sukses, melempar exception, atau dibatalkan. Contoh berikut menggunakan bentuk ketiga, dan menunggu pekerjaan latar dengan jeda singkat karena fsi berhenti begitu skripnya selesai.

fsharp Contoh: kontinuitas sukses, exception, dan pembatalan
// Tiga fungsi lanjutan untuk sukses, exception, dan pembatalan.
let kerjaLatar =
    async {
        do! Async.Sleep 100
        return "hasil latar"
    }

Async.StartWithContinuations(
    kerjaLatar,
    (fun hasil -> printfn "berhasil: %s" hasil),
    (fun ex -> printfn "gagal: %s" ex.Message),
    (fun _ -> printfn "dibatalkan"))

Async.RunSynchronously (Async.Sleep 300)
text Keluaran
berhasil: hasil latar

Satu-satunya baris keluaran berasal dari fungsi lanjutan sukses, sedangkan dua fungsi lainnya tidak terpanggil karena pekerjaan selesai tanpa error dan tanpa pembatalan. Ketika pekerjaan gagal, parameter ex pada fungsi kedua membawa exception tersebut sehingga pesannya dapat dicatat. Pola tiga fungsi ini menjaga penanganan hasil pekerjaan latar tetap eksplisit tanpa menahan thread utama. Pilihan pemulaiannya mengikuti posisi pemanggil, yaitu bentuk pemblokir pada titik masuk program dan bentuk latar pada kode pustaka yang tidak boleh menahan pemanggilnya.

Pengambilan hasil berurutan dan paralel#

Rangkaian let! menghasilkan waktu tunggu yang dijumlahkan, sedangkan Async.Parallel menjalankan kumpulan workflow secara serentak. Fungsinya bertipe seq<Async<'T>> -> Async<'T[]>, sehingga hasilnya berupa array yang urutannya mengikuti urutan masukan, bukan urutan selesai. Berkas berikut mengambil empat catatan sekaligus dengan pola tersebut.

fsharp muat-paralel.fsx
// Empat pengambilan catatan dijalankan bersamaan, hasilnya dirakit berurutan.
let muatCatatan id =
    async {
        do! Async.Sleep 100
        return sprintf "catatan %d terbaca" id
    }

let paralel =
    [ 1 .. 4 ]
    |> List.map muatCatatan
    |> Async.Parallel

Async.RunSynchronously paralel
|> Array.iter (printfn "%s")
text Keluaran
catatan 1 terbaca
catatan 2 terbaca
catatan 3 terbaca
catatan 4 terbaca

Total waktu keempat pengambilan di atas sekitar 100 milidetik karena keempatnya berjalan serentak, bukan 400 milidetik seperti versi berantai pada bagian sebelumnya. Hasil cetakan tetap berurutan karena perakitan dilakukan setelah Async.Parallel selesai, yakni array hasilnya diiterasi dari indeks pertama. Jumlah workflow yang dikirim boleh jauh lebih besar daripada jumlah thread yang tersedia pada thread pool, karena setiap workflow hanya memegang thread saat potongan kerjanya berjalan. Masukannya berupa seq<Async<'T>>, sehingga sumbernya dapat berupa list, array, atau sekuens yang dihasilkan secara dinamis. Pola kumpulkan terlebih dahulu lalu cetak ini dijaga di sepanjang modul agar keluaran setiap contoh selalu dapat direproduksi.

Model eksekusi di atas thread pool#

Model eksekusi workflow async bertumpu pada thread pool, yaitu kumpulan thread yang dikelola runtime .NET untuk menjalankan potongan kerja yang pendek. Satu workflow tersusun atas potongan-potongan kerja yang dipisahkan oleh titik tunggu, dan setiap potongan dijadwalkan pada thread yang sedang bebas. Saat Async.Sleep atau operasi I/O berlangsung, tidak ada thread yang tertahan menunggu, sehingga banyak workflow dapat menunggu serentak dengan jumlah thread yang sedikit. Setelah tunggu selesai, kelanjutan workflow dijadwalkan kembali ke thread pool untuk melanjutkan sisa badannya.

Karena itu potongan kerja di dalam workflow sebaiknya singkat, dan pekerjaan hitung yang berat ditangani dengan alat paralelisme data seperti Task.Run. Konsekuensinya, jumlah workflow yang aktif tidak lagi menggandakan jumlah thread, dan kapasitasnya dibatasi oleh memori alih-alih oleh jumlah thread.

Ringkasan#

  • async { } membentuk nilai Async<'T> yang belum berjalan, sehingga pemulaiannya eksplisit melalui Async.RunSynchronously, Async.Start, atau Async.StartWithContinuations.
  • let! dan do! merangkai workflow lain di dalam workflow, sedangkan Async.Sleep memberi jeda tanpa menahan thread.
  • Async.Parallel menjalankan banyak workflow serentak dan mengembalikan array berurutan sesuai masukan, dengan eksekusi yang bertumpu pada thread pool.

Referensi#