ลอง event loop และ non-blocking IO ใน crystal-lang

หลายครั้งผมแค่อยากได้ Ruby ที่มันเร็ว ไม่ต้องแนว ไม่ต้อง functional ก็ได้ ไม่ต้อง minimal ก็ได้

Crystal เป็นภาษาโปรแกรมแบบ static type ความเร็วแบบ thread เดียว ผมเคยเอามาลองตัดคำ ถ้าไม่ optimize อะไรเลย ความเร็วไล่ ๆ กับ Go

ประเด็น hot ช่วงนี้คือถ้าใช้แบบ concurrent จะเป็นอย่างไร Crystal ไม่ได้ใช้ multithreading แน่ ๆ เพราะมันยังทำไม่เสร็จ 🤣 แต่ว่าตอนนี้มีพวก event loop และ fiber แน่ ๆ ดูคร่าว ๆ แล้วน่าจะทำงานคล้าย ๆ Node.js ได้โดยไม่ต้องเขียน async-await

code ต่อไปนี้ผมเขียนมาลองว่าจะเป็นแบบที่คิดจริงหรือไม่

require "socket"

def load(id, chan)
  puts "ID=#{id}; BEFORE"
  (id..11).each do 
    socket = TCPSocket.new("www.ku.ac.th", 80)
    puts "ID=#{id}; Blocking?=#{socket.blocking}"
    socket.close
  end
  puts "ID=#{id}; AFTER"
  chan.send nil
end

def main
  chan = Channel(Nil).new
  (1..10).each{|i| spawn(load(i,chan))}
  # Wait
  (1..10).each{chan.receive}
end

main 

code มันก็จะคล้าย ๆ Ruby หน่อย ผมเขียนให้ ถ้า id ยิ่งน้อยยิ่งรันนาน เพื่อที่จะดูว่างานที่ spawn มาทีหลังมันมันแซงมาเสร็จก่อนได้เปล่า อีกอย่างก็คือ print มาให้ดูด้สนว่าใช้ non-blocking IO

ID=1; BEFORE
ID=2; BEFORE
ID=3; BEFORE
ID=4; BEFORE
ID=5; BEFORE
ID=6; BEFORE
ID=7; BEFORE
ID=8; BEFORE
ID=9; BEFORE
ID=10; BEFORE
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=3; Blocking?=false
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=6; Blocking?=false
ID=7; Blocking?=false
ID=8; Blocking?=false
ID=9; Blocking?=false
ID=10; Blocking?=false
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=3; Blocking?=false
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=7; Blocking?=false
ID=6; Blocking?=false
ID=8; Blocking?=false
ID=9; Blocking?=false
ID=2; Blocking?=false
ID=1; Blocking?=false
ID=10; Blocking?=false
ID=10; AFTER
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=3; Blocking?=false
ID=7; Blocking?=false
ID=6; Blocking?=false
ID=8; Blocking?=false
ID=9; Blocking?=false
ID=9; AFTER
ID=2; Blocking?=false
ID=1; Blocking?=false
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=3; Blocking?=false
ID=7; Blocking?=false
ID=6; Blocking?=false
ID=8; Blocking?=false
ID=8; AFTER
ID=2; Blocking?=false
ID=1; Blocking?=false
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=3; Blocking?=false
ID=7; Blocking?=false
ID=7; AFTER
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=6; Blocking?=false
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=3; Blocking?=false
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=6; Blocking?=false
ID=6; AFTER
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=5; AFTER
ID=3; Blocking?=false
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=4; Blocking?=false
ID=4; AFTER
ID=3; Blocking?=false
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=3; Blocking?=false
ID=3; AFTER
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=2; AFTER
ID=1; Blocking?=false
ID=1; AFTER

อันนี้ก็จะเห็นได้เลยว่า ID=1 ที่เริ่มรันก่อนมันเสร็จทีหลังเลย สรุปก็คือใน Crystal ใช้ non-blocking IO กับ event loop ได้โดยที่ programmer ไม่ต้องทำอะไรพิเศษเลย ก็แค่เขียน ๆ ไปธรรมดา ในกรณีที่เป็น web server ส่วน spawn และ channel ก็ไม่ต้องเขียนเองด้วย

Go, Erlang, Elixir ทำอะไรได้มากกว่านี้ แต่ Crystal มันเป็นภาษาที่แทบไม่ต้องปรับตัวเลย สำหรับคนที่เคยเขียน Ruby งาน CPU bound ก็น่าจะเร็วกว่า Erlang แลพ Elixir ด้วย