Bagikan melalui


Pemrograman asinkron di F#

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> dengan async { } ekspresi, yang mewakili komputasi asinkron yang dapat disusun dan dapat dimulai untuk membentuk tugas.
  • Jenis Task<'T>, dengan ekspresi task { }, 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:

  1. Ubah argumen baris perintah menjadi urutan Async<unit> komputasi dengan Seq.map.
  2. Buat sebuah Async<'T[]> yang menjadwalkan dan menjalankan komputasi printTotalFileBytes secara paralel ketika dijalankan.
  3. Async<unit> Buat yang akan menjalankan komputasi paralel dan abaikan hasilnya (yang merupakan unit[]).
  4. 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, gunakan Async.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 menggunakan Async.Start atau fungsi lain yang memerlukan Async<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 dengan Async.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:

  1. Tidak ada afinitas antara komputasi asinkron dan utas, kecuali secara eksplisit dimulai pada utas saat ini.
  2. 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.

Lihat juga