async dan await

IO asinkron dengan Future dan event loop untuk operasi jaringan yang efisien.

Daftar isi

Operasi jaringan dan berkas sering menunggu lama, dan menunggu dengan memblokir satu thread untuk setiap sambungan cepat menjadi mahal ketika jumlah koneksi bertambah. Nim menjawabnya dengan pemrograman asinkron di pustaka standar, yaitu proc yang diberi pragma {.async.} mengembalikan Future[T], kata await menunggu hasilnya tanpa membekukan program, dan satu event loop mengatur semuanya pada satu thread. Topik ini membangun dua program, yaitu tugas-tugas yang berjalan tumpang tindih, lalu server echo dan kliennya pada satu proses.

Future dan pencapaian pertama#

Proc asinkron dinyatakan dengan pragma {.async.}, dan tubuhnya boleh memakai await untuk menunggu operasi yang lambat. Nilai kembaliannya bukan T melainkan Future[T], yaitu wadah yang kelak berisi hasil, sehingga memanggil proc asinkron seketika mengembalikan kendali ke pemanggilnya sementara pekerjaan berjalan. Program pertama meluncurkan dua tugas dengan jeda berbeda, keduanya dimulai hampir bersamaan, dan hasilnya diambil dengan await pada saat dibutuhkan.

nim pesan_async.nim
# pesan_async.nim: dua tugas asinkron yang berjalan tumpang tindih

import asyncdispatch

proc tugasPesan(nama: string, jeda: int): Future[string] {.async.} =
  await sleepAsync(jeda)
  return "tugas " & nama & " selesai"

proc utama() {.async.} =
  let a = tugasPesan("a", 10)
  let b = tugasPesan("b", 20)
  echo await a
  echo await b

waitFor utama()
bash
# kompilasi dan jalankan program dua tugas asinkron
nim c --hints:off -r pesan_async.nim
text Keluaran
tugas a selesai
tugas b selesai

Kunci program itu ada pada urutan empat baris tubuh proc utama. Pemanggilan tugasPesan dua kali menyalakan dua tugas, dan jeda keduanya berjalan tumpang tindih, sehingga total menunggu mendekati jeda terlama, bukan jumlah keduanya. await tidak memblokir thread, melainkan menyerahkan kendali kembali ke event loop dan meminta dilanjutkan begitu Future-nya terisi, sedangkan waitFor di baris terakhir menjalankan event loop sampai proc asinkron teratas benar-benar selesai. Objek Future itu sendiri dapat diperiksa sebelum ditunggu, karena properti finished menyatakan apakah hasilnya sudah ada dan properti failed menyatakan apakah tubuhnya melempar error. Error yang terjadi di dalam proc asinkron tidak meledak di tempatnya, melainkan tersimpan di Future-nya dan baru muncul kembali pada titik await yang menunggunya, sehingga blok try di sekitar await adalah posisi penanganan yang benar.

Pemilihan fungsi tidur juga menyimpan perbedaan yang mudah terlewat. Proc sleepAsync menyerahkan giliran kepada event loop selama jeda berlangsung, sedangkan sleep dari modul os memblokir seluruh thread tanpa memberi giliran kepada siapa pun, sehingga satu pemanggilan sleep di dalam proc asinkron cukup untuk membekukan semua tugas lain sekaligus.

Struktur event loop#

Modul asyncdispatch menyediakan event loop yang memantau sumber IO pada satu thread. Setiap kali proc asinkron mencapai await, proc itu berhenti di titiknya, kendali kembali ke event loop, dan loop mencari tugas lain yang sumbernya sudah siap. Diagram berikut memperlihatkan siklus itu bagi tiga tugas yang bergantian.

waitFor utama()Event loop asyncdispatchsatu thread, memantau IOTugas 1await: menunggu IOTugas 2dilanjutkan event loopTugas 3await: menunggu IO
Satu event loop asyncdispatch memantau IO dan melanjutkan tugas yang Future-nya sudah terisi

Model satu thread ini menuntut disiplin yang jelas, yaitu tubuh proc asinkron tidak boleh menghabiskan waktu lama tanpa await, karena selama itu tidak ada tugas lain yang mendapat giliran. Pekerjaan berat penghitungan tetap berada di thread biasa, sedangkan pemrograman asinkron ditujukan untuk menunggu, seperti sambungan jaringan dan pembacaan berkas.

Server echo dan klien dalam satu proses#

Contoh jaringan terkecil yang utuh adalah server echo, yaitu server yang mengembalikan setiap pesan yang diterimanya. Program berikut menjalankan server dan klien pada satu proses yang sama dengan alamat 127.0.0.1, sehingga tidak memerlukan jaringan sungguhan. Server asyncCheck serverEcho() diluncurkan tanpa ditunggu, sedangkan klien dijalankan dengan waitFor karena program hanya selesai setelah percakapannya tuntas.

nim echo_lokal.nim
# echo_lokal.nim: server echo dan klien pada 127.0.0.1

import asyncdispatch, asyncnet, strutils

const portLokal = 34711

proc serverEcho() {.async.} =
  var server = newAsyncSocket()
  server.setSockOpt(OptReuseAddr, true)
  server.bindAddr(Port(portLokal), "127.0.0.1")
  server.listen()
  let klien = await server.accept()
  let pesan = await klien.recvLine()
  echo "server menerima: ", pesan.strip
  await klien.send("balasan: " & pesan & "\n")
  klien.close()
  server.close()

proc klienKirim() {.async.} =
  var klien = newAsyncSocket()
  await klien.connect("127.0.0.1", Port(portLokal))
  await klien.send("halo echo\n")
  let balasan = await klien.recvLine()
  echo "klien menerima: ", balasan.strip
  klien.close()

asyncCheck serverEcho()
waitFor klienKirim()
bash
# kompilasi dan jalankan server echo serta kliennya
nim c --hints:off -r echo_lokal.nim
text Keluaran
server menerima: halo echo
klien menerima: balasan: halo echo

Sambungan memakai modul asyncnet yang menyediakan newAsyncSocket, accept, recvLine, dan send dalam bentuk asinkronnya. recvLine menunggu sampai satu baris lengkap diterima, dan setiap pesan diakhiri \n agar sisi lawannya tahu batas barisnya. Proc asyncCheck meluncurkan proc asinkron tanpa mengambil Future-nya, sehingga kesalahan yang tidak tertangani di dalamnya tetap dilaporkan, dan pilihan ini cocok untuk server yang berjalan sebagai latar sampai program berakhir.

Kesalahan umum#

  • Memanggil proc asinkron lalu memakai hasilnya tanpa await. Nilai yang diterima adalah Future[string], dan kompilator menolaknya dengan pesan berbentuk expression 'tugasPesan("a", 10)' is of type 'Future[system.string]' and has to be used (or discarded), karena hasil yang belum terisi memang tidak boleh dipakai diam-diam.
  • Menulis pekerjaan berat di dalam proc asinkron tanpa await. Selama perhitungan itu berjalan, event loop tidak mendapat giliran sehingga seluruh tugas lain berhenti, dan pemecahannya memindahkan pekerjaan berat ke thread biasa atau memecahnya dengan await di antaranya.
  • Melupakan waitFor pada titik teratas program. Proc asinkron yang dipanggil dari kode biasa hanya mengembalikan Future yang belum berjalan, sehingga program berakhir tanpa mengerjakan apa pun, dan waitFor itulah yang menyalakan event loop sampai selesai.

Ringkasan#

  • Proc {.async.} mengembalikan Future[T], dipanggil seketika tanpa menunggu, dan hasilnya diambil dengan await yang menyerahkan kendali ke event loop alih-alih memblokir thread.
  • Event loop asyncdispatch menjalankan banyak tugas pada satu thread dengan bergantian di setiap titik await, sehingga menunggu banyak IO mendekati menunggu yang terlama, bukan jumlah semuanya.
  • Modul asyncnet menyediakan soket asinkron untuk server dan klien, asyncCheck meluncurkan tugas latar, sedangkan waitFor menjalankan tugas utama sampai benar-benar selesai.

Referensi#