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.
Pemrograman asinkron adalah mekanisme yang penting untuk aplikasi modern karena berbagai alasan. Ada dua kasus penggunaan utama yang akan dihadapi sebagian besar pengembang:
- Menyajikan proses server yang dapat melayani sejumlah besar permintaan masuk bersamaan, sambil meminimalkan sumber daya sistem yang ditempati saat pemrosesan permintaan menunggu input dari sistem atau layanan eksternal ke proses tersebut
- Mempertahankan UI responsif atau utas utama sekaligus memajukan pekerjaan latar belakang secara bersamaan
Meskipun tugas latar belakang sering melibatkan pemanfaatan beberapa utas, penting untuk mempertimbangkan konsep-konsep asinkroni dan multi-utas secara terpisah. Bahkan, mereka adalah masalah terpisah, dan satu tidak menyiratkan yang lain. Artikel ini menjelaskan konsep terpisah secara lebih rinci.
Asinkron didefinisikan
Poin sebelumnya - bahwa asinkronitas tidak bergantung pada pemanfaatan beberapa proses - perlu dijelaskan lebih lanjut. Ada tiga konsep yang terkadang terkait, tetapi benar-benar independen satu sama lain:
- Concurrency; ketika beberapa komputasi dijalankan dalam periode waktu yang tumpang tindih.
- Paralelisme; ketika beberapa komputasi atau beberapa bagian dari satu komputasi berjalan pada saat yang sama persis.
- Asinkron; ketika satu atau beberapa komputasi dapat dijalankan secara terpisah dari alur program utama.
Ketiganya adalah konsep ortogonal, tetapi dapat dengan mudah digabungkan, terutama ketika digunakan bersama- sama. Misalnya, Anda mungkin perlu menjalankan beberapa komputasi asinkron secara paralel. Hubungan ini tidak berarti bahwa paralelisme atau asinkron menyiratkan satu sama lain.
Jika Anda mempertimbangkan etymologi kata "asinkron", ada dua bagian yang terlibat:
- "a", artinya "tidak".
- "sinkron", yang berarti "pada saat yang sama".
Ketika Anda menggabungkan kedua istilah ini, Anda akan melihat bahwa "asinkron" berarti "tidak pada saat yang sama". Itu saja! Tidak ada implikasi konkurensi atau paralelisme dalam definisi ini. Ini juga berlaku dalam praktiknya.
Dalam istilah praktis, komputasi asinkron di F# dijadwalkan untuk mengeksekusi secara independen dari alur program utama. Eksekusi independen ini tidak menyiratkan konkurensi atau paralelisme, juga tidak menyiratkan bahwa komputasi selalu terjadi di latar belakang. Bahkan, komputasi asinkron bahkan dapat dijalankan secara sinkron, tergantung pada sifat komputasi dan lingkungan tempat komputasi dijalankan.
Hal utama yang perlu Anda pahami adalah bahwa komputasi asinkron terpisah dari alur program utama. Meskipun ada beberapa jaminan tentang kapan atau bagaimana komputasi asinkron dijalankan, ada beberapa pendekatan untuk mengatur dan menjadwalkannya. Sisa artikel ini mengeksplorasi konsep inti untuk asinkron F# dan cara menggunakan jenis, fungsi, dan ekspresi yang disertakan dalam F#.
Konsep inti
Dalam F#, pemrograman asinkron berpusat di sekitar dua konsep inti: komputasi asinkron dan tugas.
- Jenis
Async<'T>
denganasync { }
ekspresi, yang mewakili komputasi asinkron yang dapat disusun dan dapat dimulai untuk membentuk tugas. - Jenis
Task<'T>
, dengan ekspresitask { }
, yang mewakili tugas .NET yang sedang dieksekusi.
Secara umum, Anda harus mempertimbangkan untuk menggunakan task {…}
sebagai pengganti async {…}
dalam kode baru jika Anda berinteroperasi dengan pustaka .NET yang menggunakan task, dan jika Anda tidak mengandalkan tailcall kode asinkron atau propagasi token pembatalan implisit.
Konsep inti asinkron
Anda dapat melihat konsep dasar pemrograman "asinkron" dalam contoh berikut:
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
Dalam contoh, printTotalFileBytesUsingAsync
fungsinya berjenis string -> Async<unit>
. Memanggil fungsi sebenarnya tidak menjalankan komputasi asinkron. Sebaliknya, ia mengembalikan Async<unit>
yang bertindak sebagai spesifikasi pekerjaan yang akan dijalankan secara asinkron. Ini memanggil Async.AwaitTask
dalam tubuhnya, yang mengonversi hasilnya ReadAllBytesAsync menjadi jenis yang sesuai.
Baris penting lainnya adalah panggilan ke Async.RunSynchronously
. Ini adalah salah satu fungsi awal modul Asinkron yang perlu Anda panggil jika Anda ingin benar-benar menjalankan komputasi asinkron F#.
Ini adalah perbedaan mendasar dengan gaya pemrograman async
C#/Visual Basic. Dalam F#, komputasi asinkron dapat dianggap sebagai tugas Dingin. Mereka harus memulai secara eksplisit agar benar-benar dapat dieksekusi. Ini memiliki beberapa keuntungan, karena memungkinkan Anda untuk menggabungkan dan mengurutkan pekerjaan asinkron jauh lebih mudah daripada di C# atau Visual Basic.
Menggabungkan komputasi asinkron
Berikut adalah contoh yang dibangun di atas yang sebelumnya dengan menggabungkan komputasi:
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
Seperti yang main
Anda lihat, fungsi ini memiliki beberapa elemen lagi. Secara konseptual, ini melakukan hal berikut:
- Ubah argumen baris perintah menjadi urutan
Async<unit>
komputasi denganSeq.map
. - Buat sebuah
Async<'T[]>
yang menjadwalkan dan menjalankan komputasiprintTotalFileBytes
secara paralel ketika dijalankan. -
Async<unit>
Buat yang akan menjalankan komputasi paralel dan abaikan hasilnya (yang merupakanunit[]
). - Jalankan secara eksplisit perhitungan yang disusun secara keseluruhan dengan
Async.RunSynchronously
dan tahan hingga proses selesai.
Ketika program ini berjalan, printTotalFileBytes
berjalan secara paralel untuk setiap argumen baris perintah. Karena komputasi asinkron dijalankan secara independen dari alur program, tidak ada urutan yang ditentukan di mana mereka mencetak informasi mereka dan menyelesaikan eksekusi. Komputasi akan dijadwalkan secara paralel, tetapi urutan eksekusinya tidak dijamin.
Mengurutkan komputasi asinkron
Karena Async<'T>
merupakan spesifikasi pekerjaan daripada tugas yang sudah berjalan, Anda dapat melakukan transformasi yang lebih rumit dengan mudah. Berikut adalah contoh yang mengurutkan satu set komputasi Asinkron sehingga dijalankan satu demi satu.
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
Ini akan dijadwalkan printTotalFileBytes
untuk dijalankan dalam urutan elemen argv
daripada menjadwalkannya secara paralel. Karena setiap operasi berturut-turut tidak akan dijadwalkan sampai setelah komputasi sebelumnya selesai dieksekusi, komputasi diurutkan sedami sehingga tidak ada tumpang tindih dalam eksekusi mereka.
Fungsi modul Asinkron penting
Saat menulis kode asinkron di F#, Anda biasanya akan berinteraksi dengan kerangka kerja yang menangani penjadwalan komputasi untuk Anda. Namun, ini tidak selalu terjadi, jadi ada baiknya untuk memahami berbagai fungsi yang dapat digunakan untuk menjadwalkan pekerjaan asinkron.
Karena komputasi asinkron F# adalah spesifikasi pekerjaan daripada representasi pekerjaan yang sudah dijalankan, mereka harus secara eksplisit dimulai dengan fungsi awal. Ada banyak metode awal Asinkron yang berguna dalam konteks yang berbeda. Bagian berikut menjelaskan beberapa fungsi awal yang lebih umum.
Async.StartChild
Memulai komputasi anak dalam komputasi asinkron. Ini memungkinkan beberapa komputasi asinkron dijalankan secara bersamaan. Komputasi anak berbagi token pembatalan dengan komputasi induk. Jika komputasi induk dibatalkan, komputasi anak juga dibatalkan.
Tanda tangan:
computation: Async<'T> * ?millisecondsTimeout: int -> Async<Async<'T>>
Kapan menggunakan:
- Ketika Anda ingin mengeksekusi beberapa komputasi asinkron secara bersamaan daripada satu per satu, tanpa menjadwalkannya secara paralel.
- Ketika Anda ingin mengikat masa pakai komputasi anak dengan komputasi induk.
Apa yang harus diwaspadai:
- Memulai beberapa komputasi dengan
Async.StartChild
bukan berarti menjadwalkannya secara paralel. Jika Anda ingin menjadwalkan komputasi secara paralel, gunakanAsync.Parallel
. - Membatalkan komputasi induk akan memicu pembatalan semua komputasi anak yang dimulainya.
Async.StartImmediate
Menjalankan komputasi asinkron, dimulai segera pada utas sistem operasi saat ini. Ini berguna jika Anda perlu memperbarui sesuatu pada utas panggilan selama komputasi. Misalnya, jika komputasi asinkron harus memperbarui UI (seperti memperbarui bilah kemajuan), maka Async.StartImmediate
harus digunakan.
Tanda tangan:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Kapan menggunakan:
- Ketika Anda perlu memperbarui sesuatu pada utas panggilan di tengah komputasi asinkron.
Apa yang harus diwaspadai:
- Kode dalam komputasi asinkron akan berjalan pada utas apa pun yang kebetulan dijadwalkan. Ini bisa bermasalah jika utas tersebut sensitif, seperti utas UI. Dalam kasus seperti itu,
Async.StartImmediate
kemungkinan tidak pantas untuk digunakan.
Async.StartAsTask
Melaksanakan perhitungan dalam kumpulan thread. Mengembalikan Task<TResult> yang akan diselesaikan pada status yang sesuai setelah komputasi berakhir (menghasilkan hasil, melemparkan pengecualian, atau dibatalkan). Jika tidak ada token pembatalan yang disediakan, maka token pembatalan default akan digunakan.
Tanda tangan:
computation: Async<'T> * ?taskCreationOptions: TaskCreationOptions * ?cancellationToken: CancellationToken -> Task<'T>
Kapan menggunakan:
- Ketika Anda perlu memanggil .NET API yang menghasilkan Task<TResult> yang mewakili hasil komputasi asinkron.
Apa yang harus diwaspadai:
- Panggilan ini akan mengalokasikan objek tambahan
Task
, yang dapat meningkatkan overhead jika sering digunakan.
Asinkron.Paralel
Menjadwalkan urutan komputasi asinkron yang akan dijalankan secara paralel, menghasilkan array hasil dalam urutan yang disediakan. Tingkat paralelisme dapat disetel/dibatasi secara opsional dengan menentukan maxDegreeOfParallelism
parameter .
Tanda tangan:
computations: seq<Async<'T>> * ?maxDegreeOfParallelism: int -> Async<'T[]>
Kapan menggunakannya:
- Jika Anda perlu menjalankan sekumpulan komputasi secara bersamaan dan tidak memiliki keterganungan pada urutan eksekusinya.
- Jika Anda tidak memerlukan hasil dari komputasi yang dijadwalkan secara paralel hingga semuanya selesai.
Apa yang harus diwaspadai:
- Anda hanya dapat mengakses array nilai yang dihasilkan setelah semua komputasi selesai.
- Komputasi akan dijalankan setiap kali dijadwalkan. Perilaku ini berarti Anda tidak dapat mengandalkan urutan eksekusi mereka.
Asinkron.Berurutan
Menjadwalkan urutan komputasi asinkron yang akan dijalankan dalam urutan yang diteruskan. Komputasi pertama akan dijalankan, kemudian berikutnya, dan sebagainya. Tidak ada komputasi yang akan dijalankan secara paralel.
Tanda tangan:
computations: seq<Async<'T>> -> Async<'T[]>
Kapan menggunakannya:
- Jika Anda perlu menjalankan beberapa komputasi secara berurutan.
Apa yang harus diwaspadai:
- Anda hanya dapat mengakses array nilai yang dihasilkan setelah semua komputasi selesai.
- Komputasi akan dijalankan dalam urutan yang diteruskan ke fungsi ini, yang dapat berarti bahwa lebih banyak waktu akan berlalu sebelum hasilnya dikembalikan.
Async.AwaitTask
Mengembalikan komputasi asinkron yang menunggu Task<TResult> yang diberikan selesai dan mengembalikan hasilnya sebagai Async<'T>
Tanda tangan:
task: Task<'T> -> Async<'T>
Kapan menggunakan:
- Saat Anda menggunakan .NET API yang mengembalikan Task<TResult> dalam komputasi asinkron F#.
Apa yang harus diwaspadai:
- Pengecualian dibungkus dalam AggregateException mengikuti konvensi Task Parallel Library; perilaku ini berbeda dari bagaimana async F# umumnya menampilkan pengecualian.
Asinkron.Catch
Membuat komputasi asinkron yang menjalankan yang diberikan Async<'T>
, mengembalikan Async<Choice<'T, exn>>
. Jika yang diberikan Async<'T>
berhasil diselesaikan, maka dikembalikan Choice1Of2
dengan nilai yang dihasilkan. Jika pengecualian dilemparkan sebelum selesai, maka Choice2of2
dikembalikan dengan pengecualian yang dihasilkan. Jika digunakan pada komputasi asinkron yang terdiri dari banyak komputasi, dan salah satu komputasi tersebut memberikan pengecualian, komputasi yang mencakup akan dihentikan sepenuhnya.
Tanda tangan:
computation: Async<'T> -> Async<Choice<'T, exn>>
Kapan menggunakan:
- Saat Anda melakukan pekerjaan asinkron yang mungkin gagal dengan pengecualian dan Anda ingin menangani pengecualian tersebut di pemanggil.
Apa yang harus diwaspadai:
- Saat menggunakan komputasi asinkron yang digabungkan atau diurutkan, komputasi yang menyeluruh akan sepenuhnya berhenti jika salah satu komputasi "internal" melempar pengecualian.
Asinkron.Abaikan
Membuat komputasi asinkron yang menjalankan komputasi yang diberikan tetapi menghilangkan hasilnya.
Tanda tangan:
computation: Async<'T> -> Async<unit>
Kapan menggunakan:
- Ketika Anda memiliki komputasi asinkron yang hasilnya tidak diperlukan. Ini dianalogikan dengan
ignore
fungsi untuk kode non-asinkron.
Apa yang harus diwaspadai:
- Jika Anda harus menggunakan
Async.Ignore
karena Anda ingin menggunakanAsync.Start
atau fungsi lain yang memerlukanAsync<unit>
, pertimbangkan jika membuang hasilnya baik-baik saja. Hindari membuang hasil hanya untuk menyesuaikan signature tipe.
Async.RunSynchronously
Menjalankan komputasi asinkron dan menunggu hasilnya pada utas panggilan. Menyebarkan pengecualian jika komputasi menghasilkan satu. Panggilan ini bersifat memblokir.
Tanda tangan:
computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T
Kapan menggunakannya:
- Jika Anda membutuhkannya, gunakan hanya sekali dalam aplikasi - pada titik masuk untuk program yang dapat dijalankan.
- Ketika Anda tidak peduli tentang performa dan ingin menjalankan serangkaian operasi asinkron lainnya sekaligus.
Apa yang harus diwaspadai:
- Memanggil
Async.RunSynchronously
akan memblokir utas pemanggil hingga eksekusi selesai.
Async.Start
Memulai komputasi asinkron yang menghasilkan unit
di kumpulan utas. Tidak menunggu penyelesaiannya dan/atau mengamati hasil pengecualian. Komputasi berlapis yang dimulai dengan Async.Start
dimulai secara independen dari komputasi induk yang memanggilnya; masa operasinya tidak terkait dengan komputasi induk mana pun. Jika komputasi induk dibatalkan, tidak ada komputasi anak yang dibatalkan.
Tanda tangan:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Gunakan hanya ketika:
- Anda memiliki komputasi asinkron yang tidak menghasilkan hasil dan/atau memerlukan pemrosesan satu.
- Anda tidak perlu tahu kapan komputasi asinkron selesai.
- Anda tidak peduli pada utas mana komputasi asinkron dijalankan.
- Anda tidak perlu mengetahui atau melaporkan pengecualian yang dihasilkan dari eksekusi.
Apa yang harus diwaspadai:
- Pengecualian yang dimunculkan oleh komputasi yang dimulai dengan
Async.Start
tidak disebarkan ke pemanggil. Tumpukan panggilan akan dibongkar sepenuhnya. - Pekerjaan apa pun, seperti panggilan
printfn
, yang dimulai denganAsync.Start
tidak akan menyebabkan efek terjadi pada utas utama eksekusi program.
Berinteroperasi dengan .NET
Jika menggunakan async { }
pemrograman, Anda mungkin perlu beroperasi dengan pustaka .NET atau basis kode C# yang menggunakan pemrograman asinkron gaya async/await. Karena C# dan sebagian besar pustaka .NET menggunakan jenis Task<TResult> dan Task sebagai abstraksi intinya, hal ini dapat mengubah cara Anda menulis kode asinkron F# Anda.
Salah satu opsinya adalah beralih menulis tugas .NET secara langsung menggunakan task { }
. Atau, Anda dapat menggunakan Async.AwaitTask
fungsi untuk menunggu komputasi asinkron .NET:
let getValueFromLibrary param =
async {
let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
return value
}
Anda dapat menggunakan Async.StartAsTask
fungsi untuk meneruskan komputasi asinkron ke pemanggil .NET:
let computationForCaller param =
async {
let! result = getAsyncResult param
return result
} |> Async.StartAsTask
Untuk bekerja dengan API yang menggunakan Task (yaitu, komputasi asinkron .NET yang tidak mengembalikan nilai), Anda mungkin perlu menambahkan fungsi tambahan yang akan mengonversi Async<'T>
menjadi Task:
module Async =
// Async<unit> -> Task
let startTaskFromAsyncUnit (comp: Async<unit>) =
Async.StartAsTask comp :> Task
Sudah ada Async.AwaitTask
yang menerima Task sebagai input. Dengan fungsi ini dan yang sudah ditentukan startTaskFromAsyncUnit
sebelumnya, Anda dapat memulai dan menunggu jenis Task dari komputasi asinkron dalam F#.
Menulis tugas .NET langsung di F#
Di F#, Anda dapat menulis tugas secara langsung menggunakan task { }
, misalnya:
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
Dalam contoh, printTotalFileBytesUsingTasks
fungsinya berjenis string -> Task<unit>
. Memanggil fungsi akan mulai menjalankan tugas.
Panggilan untuk task.Wait()
menunggu tugas selesai.
Hubungan dengan multi-utas
Meskipun "threading" disebutkan di seluruh artikel ini, ada dua hal penting yang perlu diingat:
- Tidak ada afinitas antara komputasi asinkron dan utas, kecuali secara eksplisit dimulai pada utas saat ini.
- Pemrograman asinkron dalam F# bukanlah abstraksi untuk multi-threading.
Misalnya, komputasi dapat benar-benar berjalan pada utas pemanggilnya, tergantung pada sifat tugas. Komputasi juga dapat "melompat" di antara utas, meminjamnya untuk sedikit waktu untuk melakukan pekerjaan yang berguna di antara periode "menunggu" (seperti ketika panggilan jaringan sedang transit).
Meskipun F# memberikan beberapa kemampuan untuk memulai komputasi asinkron pada utas saat ini (atau secara eksplisit tidak pada utas saat ini), asinkron umumnya tidak terkait dengan strategi threading tertentu.