veer66

veer66

AFAIK Generating UUID and Xid needs the MachineGuid, so I looked around how to obtain it.

And this is the solution, which I'm not sure if it will work on every Windows machine. 😅

main.rs

use winreg::RegKey;
use winreg::enums::*;
use std::io;

fn main() -> io::Result<()> {
    let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
    let cur_ver = hklm.open_subkey("SOFTWARE\\Microsoft\\Cryptography")?;
    let guid: String = cur_ver.get_value("MachineGuid")?;
    println!("MachineGuid = {}", guid);
    Ok(())
}

Cargo.toml

[package]
name = "mach_guid"
version = "0.1.0"
authors = ["veer6"]
edition = "2018"

[dependencies]
winreg = "0.6"

เอกสารเดี๋ยวนี้อาจจะไม่ได้มาแบบเป็นเล่ม ๆ อย่างเดียว บางคนอาจจะใช้ E-book reading จอ 6 นิ้วอ่าน บางคนอาจจะ print ใส่กระดาษ A4 บางคนอาจจะอ่านใน Tablet บางคนอาจจะอ่านบนจอใหญ่ บางคนอาจจะอ่านในโทรศัพท์มือถือ

มันคงไม่ค่อยดีเท่าไหร่ที่จะต้องมาจัดหน้าให้ทุกแบบเอง คนที่เขียนเนื้อหาก็ควรจะเขียนเนื้อหาไป ไม่ต้องมากังวลกับการจัดหน้ามาก

MS Word มันกึ่ง ๆ ระหว่างจะเน้นเนื้อหา หรือว่าจะเน้นจัดหน้าดี จะเลือกว่าส่วนไหนเป็น heading ก็ได้ แต่นึกอยากจะกำหนด font กำหนดขนาดตัวอักษรของแต่ส่วนเองก็ได้อีก

จะใช้ Word แบบเน้นเนื้อหาก็ได้ โดยการเลือก style แทนที่จะไปจัดเอง แต่พอทำจริง ๆ ส่วนมากออกมา format ไม่เป๊ะ มันมีวิธีหลายแบบที่จะทำให้เอกสารออกมาหน้าตาคล้าย ๆ กัน มอง style ก็ไม่ค่อยจะเห็นชัดเท่าไหร่ ทีนี้เอามาประมวลผล หรือแค่จัดรูปแบบใหม่ให้เหมาะกับ online หรือแม้กระทั่ง ebook reader จอเล็ก ๆ มันจะเละ

มันมีสิ่งที่คิดมาแก้เรื่องนี้หลายตัวเช่น GNU Texinfo มันก็ใช้ได้เอกสารของ GNU Project ทั้งหมดใช้ตัว Texinfo เวลาดูเว็บของ GNU หน้าตามันจะซ้ำ ๆ เบื่อ ๆ หน่อย แต่ก็เป็นวิธีที่ทำเอกสารที่ทำทีเดียว ออกมาได้ทั้ง HTML, Info, man ทำเป็น PDF ก็ได้ ปัญหาของ Texinfo คือกว่าจะเขียนเป็นมันก็ไม่ได้ยาก แต่ก็ยากกว่า Word

อีกตัวคือ Gitbook อันนี้ใช้เขียนหนังสือเป็นเล่ม ๆ ได้เลย แล้วก็เอามาสร้างได้หลาย format คล้าย ๆ กับ GNU Texinfo แต่ว่าใช้ Markdown คนทาง IT เดี๋ยวนี้หลาย ๆ คนก็เขียน Markdown เป็นเลย เป็น format ที่ดูง่ายมาก ๆ บางคนอาจจะบอกว่าเปิด docx ออกมาอ่าน format ข้างในก็ได้เหมือนกัน แต่มันก็อ่านยากกว่า Markdown มาก ๆ แต่ถึง Markdown จะง่ายอย่างไรมันก็ยากกว่าใช้ Word อยู่ดี Gitbook ในเว็บเขามีตัวแก้เอกสารแบบ WYSIWYG มาให้เลย แต่ format มันก็จะเป๊ะอยู่ดี WYSIWYG ของเขาไม่ใช่แบบที่ให้มาเลือก font จัดโน่นจัดนี่เอง แต่พอเป็นตัวหนังสือก็จะเลือกได้ว่าจะเป็น Heading 1, Heading 2, Heading 3 อะไรทำนองนั้นไป

ผมมองว่าแบบ Gitbook นี่ล่ะที่น่าจะเหมาะเพราะ

  1. แยกกันระหว่างเนื้อหาและการจัดหน้า
  2. แก้ไขแบบเห็นเหมือนจริง (WYSIWYG) ได้เลย ทำให้ง่าย
  3. ตรวจ format ง่าย เพราะว่าถึงจะเปิดแบบ Markdown มาก็พอจะอ่านรู้เรื่องเลย ทำให้รู้ว่ามันเก็บอะไรลงไปกันแน่
  4. อีกข้อหนึ่งที่แถมคือเวลาแก้ไขเอกสารด้วยกันหลาย ๆ คนพร้อมกันผ่านเว็บนี่มันสะดวกกว่าการส่งไฟล์กันไปมาเยอะแยะ

ผมว่าเทคโนโลยีมันพร้อมแล้ว เหลือแต่ว่าคนจะอยากเปลี่ยนเมื่อไหร่

update 2019-08-04

ยกตัวอย่างเพิ่มเติมเพื่อให้เห็นภาพว่า markdown มันเพิ่มเพิ่มขนาด font ตรง ๆ ไม่ได้ ต้องไปทำผ่านการสร้าง header ทำให้เวลาเอาไปใช้ไม่ต้องมีตีความว่า TH Sarabun PSK 24 นี่มันคืออะไร เป็น header หรือเปล่า หรือ header ระดับไหน

สมัยที่เรียนหนังสือเขามักจะเอา Fibonacci number มาเป็นตัวอย่างกัน function ก็เป็นงี้ครับ

fib(0) = 0 fib(1) = 1 fib(n) = fib(n-1) + fib(n-2) สำหรับ n มากกว่า 1

ซึ่งเอามาแปลงเป็น Clojure ได้แบบตรงไปตรงมาเลย

(def fib
  (fn [n]
    (if (<= n 1) n
        (+ (fib (- n 1)) (fib (- n 2))))))

ผมลองรันบนเครื่องผมใช้เวลา 4.324 วินาที ซึ่งมันช้าเพราะไปเรียก fib ซ้ำไปซ้ำมา ดูง่าย ๆ fib(4) ก็ไปเรียก fib(3), fib(2) แล้ว fib(3) ก็ไปเรียก fib(2) อีกซ้ำ พอเลขมากก็ซ้ำไปเรื่อย ๆ

จริง ๆ มันมีวิธีหล่อ ๆ เยอะแยะที่จะแก้ปัญหานี้ให้เร็วขึ้น แต่ถ้าเราไม่ต้องหล่อมาแก้แบบดอกเดียวเสร็จก็ใช้ memoize เขียนแบบนี้

(def fib
  (memoize
   (fn [n]
     (if (<= n 1) n
         (+ (fib (- n 1))
            (fib (- n 2)))))))
(println (time (fib 42)))

แก้นิดเดียวเอา memoize มาครอบ function ไว้ มันจะจำ function ที่เคยเรียกไว้แล้วส่งผลกลับมาเลยโดยไม่ต้องไล่ยาว ผลคือโปรแกรมใช้เวลารันเหลือ 0.820 วินาที

จริง ๆ มันจบแล้วล่ะ แต่เขียนให้ดูอีก function แบบใช้ loop

(defn fib [n]
  (loop [i 1 acc0 0 acc1 1]
    (if (< i n)
      (recur (inc i) acc1 (+ acc0 acc1))
      acc1)))      
(println (time (fib 42)))

อันนี้ใช้เวลาแค่ 0.0976 วินาที แต่ก็จะเห็นได้ว่าโปรแกรมเปลี่ยนไปเยอะ เปลี่ยนวิธีคิดเยอะ memoize นี่มันแทบไม่ต้องคิดต้องเขียนอะไรมากเลย ใส่ไปแล้วได้ผลเลย

ขอบคุณ visibletrap@twitter.com ที่แนะนำ function time มาครับ

ผมมักจะไปสร้าง script ด้วย Python สั้น ๆ ไว้ใน $HOME/bin หรือ /usr/local/bin ก็ได้ แล้วก็ chmod 755 <ชื่อไฟล์> แล้วก็จะได้เรียกจาก shell ได้เลย

ผมเขียนไว้ 3 ตัวตามนี้

/usr/local/bin/deepcut

#!/usr/bin/env python
# * mode: python-mode *

import deepcut
import json
import sys

for line in sys.stdin:
    line = line.strip()
    print(json.dumps(deepcut.tokenize(line)))

/usr/local/bin/newmm

#!/usr/bin/env python
# * mode: python-mode *

from pythainlp.tokenize import word_tokenize

import json
import sys

for line in sys.stdin:
    line = line.strip()
    print(json.dumps(word_tokenize(line ,engine='newmm')))

/usr/local/bin/wordcutpy

#!/usr/bin/env python
# * mode: python-mode *

from wordcut import Wordcut
import json
import sys

wordcut = Wordcut.bigthai()

for line in sys.stdin:
    line = line.strip()
    print(json.dumps(wordcut.tokenize(line)))

เรียกใช้งาน

ใน shell จะเรียกเดี่ยว ๆ ก็ได้แบบ

$ newmm < file1.txt > file1.ndjson

แต่ส่วนมากก็จะเรียกผ่าน for-loop เถื่อน ๆ แบบนี้

$ for x in *.txt; do newmm < $x > $x.ndjson; done

พอได้ไฟล์มาก็เอาไปใช้กับภาษาอื่นต่อได้

/usr/local/bin/wcnt

#!/usr/bin/env ruby
# * mode: ruby-mode *

require "json"
require "csv"

h = Hash.new(0)
while gets
  JSON.parse($_.chomp)
    .each{|w| h[w.chomp] += 1}
end
h.to_a.each{|row| puts CSV::generate_line(row)}

เวลาเรียกก็ไป pipe แบบนี้เลย

cat file1.txt | newmm | wcnt

เดี๋ยวนี้คนดี ๆ เขาคงใช้ Jupyter กันหมดแล้ว ผมก็พยายามฝึกอยู่เหมือนกัน แต่ยังไม่คล่อง บางทีก็เลยเอาแบบนี้ไปก่อน

I failed to download a list of PDFs by Wget, cURL, and Slimerjs. So I used Firefox and shell script.

First, I disable Pdfjs to force Firefox to download PDFs.

I wrote a shell script for opening a URL with PDF file, and then I use xdotool to close the Firefox window. For xdotool command, thank Jon G – Megaphone Tech for his comment at Unix Exchange.

#!/bin/sh

for url in `sort -u eng_pdf_urls.txt | grep -i pdf`
do
    echo $url
    firefox -P exp1 "$url" &
    sleep 10
    echo CLOSE
    xdotool search "Mozilla Firefox" windowactivate --sync key --window 0 --clearmodifiers alt+F4
    sleep 10
done

I didn't use headless because I don't know how to close it properly. I tried timeout command but Firefox detected it as an improper shutdown.

I ported my program for showing concurrency from Erlang to Elixir. I guess this is still not Idiomatic Elixir. 😅

defmodule Con1 do
  def log(p, i) when rem(i, 3000) == 0 do
    IO.puts "#{p} #{i}"
  end
  def log(_, _) do
    :ok
  end

  def cnt(_, 0) do
    :ok
  end
  def cnt(p, i) do
    log(p, i)
    cnt(p, i - 1)
  end

  def par_cnt(0) do
    :ok
  end
  def par_cnt(n) do
    spawn fn() -> cnt(n, 3000000) end
    par_cnt(n - 1)
  end
end

Crystal intentionally looks similar to Ruby. However, Crystal is a static typing programming language. A Thai word segmentation program written in Crystal, which is a CPU bound program, was found almost as fast as one written in Go.

I wondered whether its concurrency support was like what I expected. So I did this experiment. By spawning, Crystal did not create any new thread because it doesn't support multithreading parallelism yet. To support concurrency, Crystal has to be able to switch fibers when a fiber performs non-blocking IO operation.

require "socket"

def load(id, chan)
  puts "ID=#{id}; START"
  (id..11).each do 
    socket = TCPSocket.new("www.ku.ac.th", 80)
    socket.close
  end
  puts "ID=#{id}; FINISH"
  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 

In this program, a task with the id with a smaller number creates a TCP socket repeatedly more times than a task with a larger number id. For example task#1 establishes a TCP socket for 11 times and task#10 create a TCP socket for one time only. So even task#1 started long before task#10, task#10 should finish before task#1.

ID=1; START
ID=2; START
ID=3; START
ID=4; START
ID=5; START
ID=6; START
ID=7; START
ID=8; START
ID=9; START
ID=10; START
ID=10; FINISH
ID=9; FINISH
ID=8; FINISH
ID=7; FINISH
ID=6; FINISH
ID=5; FINISH
ID=4; FINISH
ID=3; FINISH
ID=2; FINISH
ID=1; FINISH

The result was that task#1 finished after task#10 although task#1 started before task#10, show that Crystal can switch tasks. And the important thing is the code looks like regular Ruby code, without async-await nor promise nor callback.

หลายครั้งผมแค่อยากได้ 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 ด้วย

ผมจะใช้คำศัพท์ thread และ process ในบริบทของ Linux และ *BSD นะครับ ใน OS อื่น ๆ บางตัวเรียกไม่เหมือนกัน

nginx และ thread pool

  • nginx ใช้ event loop ด้วย ใช้หลาย thread ด้วยครับ 1
  • ผมเข้าใจว่าใช้ event loop เดียวรับ request แล้วไปปล่อย task เข้า thread อื่นอีกที แล้วส่งผลคิน event loop

node.js cluster

  • node.js cluster แตก process (แทนที่จะแตก thread) ครับ มี process หลักมา accept incoming connection แล้วส่งต่อให้ process ลูกครับ2
  • แต่ว่าไม่แน่ใจว่ามี event loop ของแต่ละ process อีกทีหรือเปล่าครับ ผมเดาว่าน่าจะมีแต่ก็ไม่เคยลองดูจริง ๆ ครับ

Tokio

  • Tokio อย่างน้อยจะต่างจาก node cluster ตรงที่ไม่ได้แตก process ครับ แต่ว่าใช้แตก thread ในแง่นี้น่าจะคล้าย nginx มากกว่า node cluster
  • ข้อมูลอีกอย่างคือมี event loop เท่าจำนวน thread ที่แตกออกมาครับ แต่ส่วนนี้ผมยังไม่ได้ดูว่าแบ่งงานกันอย่างไรครับ

CGI

ส่วนพวก CGI คือแตก process (ไม่ใช่ thread) ทุกครั้งที่มี request ครับ

PHP

  • php-fpm แตก process มารับ request
  • mod_php บน Apache ก็แตก process เหมือนกันครับ เพราะว่า php ต้องการแบ่งแยกระดับ process
  • พวกนี้ก็จะทำให้เร็วขึ้นโดยใช้ prefork ไว้ก่อนทำให้เร็วขึ้นได้
  • ใช้ process pool ไม่ได้ fork หมดเหมือน CGI แต่ก็เปลือง RAM อยู่ดีครับ
  • นอกจากนั้นเพื่อที่จะทำให้เร็วขึ้นมันมี opcache ที่ compile โปรแกรมเก็บไว้ก่อนรับ request อยู่แล้วครับ

Gunicorn และ Meinheld

gnuicorn ใช้ process มาเป็น worker โดยที่ worker ที่ใช้ meinheld ข้างในก็ไปใช้ picoev เอามาทำ event loop อีกที

ผมไม่ค่อยแม่นเรื่องนี้เท่าไหร่ถ้ามีอะไรมั่วไปก็ขออภัยด้วยครับ สามารถพูดคุยกันเพิ่มเติมได้ที่ veer66@mastodon.in.th

เขียนโปรแกรมบน web server ยุค 90 ส่วนมากก็ผ่าน CGI เอาครับ สมัยนั้นใช้ C, Perl, Bash เขียนกัน URL มันก็จะประมาณ http://localhost/cgi-bin/do_sth.pl งี้เลย web server มันก็จะเรียก do_sth.pl ให้อีกที ถ้าเป็นบน GNU/Linux ก็คือต้อง fork/spawn process ขึ้นมาใหม่เลย

ปลาย ๆ ยุค 90 ค่าย Java ก็มีตัวเลือกมาให้อีกแบบคือใช้ Servlet ซึ่งตัวนี้ใช้หลาย ๆ thread เอาแทนทีจะมา spawn process ใหม่ ซึ่งผมเดาว่าเวลา implement เขาก็น่าจะมี thread pool ตั้งแต่ตอนนั้น พูดอีกอย่างคือเอา thread สร้างมาแล้วมาใช้ซ้ำ

PHP ก็มาปลาย ๆ ยุค 90 เหมือนกัน แต่ผมเริ่มใช้ทีแรกก็เป็น mod_php ที่ฝังมาใน Apache Httpd แล้ว เวลาใช้จริง ๆ มันก็ fork process ใหม่อยู่ดี แต่ว่าก็มี prefork ไว้ก่อนได้ ถ้าผมเข้าใจไม่ผิดแต่ละ process ก็จะมี PHP runtime พร้อม extension ต่าง ๆ อยู่ใน memory ด้วย อันนี้น่าจะเป็นเหตุผลหลัก ๆ ที่หลาย ๆ ท่านบ่นกันว่ามันกิน RAM

ยุค 90 MS ก็มี ASP ออกมา แต่ผมไม่รู้เลยว่าทำงานอย่างไร ก็เลยข้ามไปแล้วกัน

หลังจากที่ไม่ค่อยจะมีอะไรน่าสนใจออกมาสักพัก กลาง ๆ ยุค 2000 ก็มี Ruby on Rails ออกมา ผลที่ตามมาคือมี web server ที่เอามาใช้รัน Rails ได้หลายตัว ซึ่งผมไม่รู้เลยว่าตอนนั้นเขาทำแบบไหนกัน 😅; ตอนนั้นผมใช้ TurboGears ก่อน 1.0 (บน Python) ส่วน Web Server ใช้ CherryPy ผมลองดู version เก่า ๆ ก็ใช้หลาย thread แล้วมี threadpool ด้วย

ราว ๆ ค.ศ. 2008 ผมก็เริ่มได้ยินพี่ป็อกพูดถึง Erlang ซึ่ง Erlang นี่ผมก็เพิ่งมาทราบทีหลังว่ามาในชุมสายโทรศัพท์ตั้งแต่ยุค 80 แล้ว ใน Erlang ใช้คำว่า process แทน thread แต่ว่าเอาจริง ๆ มันไม่ใช่ process ของ Linux หรือ BSD แบบของ Apache Httpd มันคล้าย green thread ของ Java ยุคแรก ๆ มากกว่า process ของ Linux แต่ว่าเรียกว่า process เพราะว่าไม่ได้ share memory แบบที่ thread ส่วนมากทำ Erlang process ใช้ memory อยู่ในวิสัยที่จะ spawn มาเป็นแสนตัวได้ สลับ ๆ ไปสลับมาแบบ preemptive ต่อให้ไม่มี I/O หรือไปสั่งให้สลับ scheduler มันก็จะสลับให้ แต่ถึงมันจะสร้าง process ได้เยอะแยะแต่ก็เหมือนว่าเขาจะไม่ได้สร้างกันทุกครั้งที่ request เข้ามา แต่ว่าไปใช้ gen_server ซึ่งถ้าผมเข้าใจไม่ผิดมันทำให้ reuse process เดิมได้

ยุคหลัง 2010 คนเริ่มเปลี่ยนมาใช้ node.js ซึ่งใช้ประโยชน์จาก kernel polling เช่น epoll และ kqueue เอามาทำ event loop พอมีงานที่ต้องรอ I/O ก็เอาออกไปรอก่อนแล้วสลับไปรับงานอื่นได้ แต่ว่า JavaScript แต่เดิมมันแตก thread ไม่ได้ node.js ก็แตก thread ไม่ได้เหมือนกัน เวลาจะใช้ประโยชน์จาก CPU หลาย ๆ core ก็เลยต้องแตก process ของ Linux หรือ OS อื่น ๆ เอาแล้วก็มี runtime ไปอยู่ทุก process เลย

พอเริ่มใช้ node.js ไม่เท่าไหร่ ก็มี Golang จาก Google ออกมา ซึ่งจะมองว่า Golang ก็คือแบบที่ใช้ multithread เลยก็ได้ พวกตัวที่เร็ว ๆ เช่น fasthttp ก็มี thread pool คล้าย ๆ กับ web server ก่อนหน้านี้ แต่ว่า thread ของ Golang หรือที่เรียกว่า Goroutine มันใช้ memory น้อยไม่ใช่ thread ของ OS ตรง ๆ ทำให้ spawn ออกมาเยอะ ๆ ได้ fasthttp อาจจะเป็นไปได้ว่า spawn ออกมาหลักแสนตัวได้เลย แล้วก็ปล่อย scheduler ของ Go runtime ไปจัดลง thread ของ OS เอาเอง จะเห็นว่าคล้าย ๆ Erlang อยู่บ้าง แต่ว่า Goroutine อย่างน้อยก็ช่วงนี้ยังไม่ได้สลับแบบ preemptive

วงการ Rails เองก็มีการปรับตัว Puma ที่เป็น default web server ทุกวันนี้ก็เป็นแบบ multithread โดยที่ค่า default คือให้เปิดมาได้เลย 5 thread แต่ใน production จะใช้มากกว่านี้ก็ได้ เทียบกับตัวเก่า ๆ Puma รับ concurrency ได้มากขึ้นใช้ RAM น้อยลง เท่าที่ผมเห็นก็คือมีทั้ง worker thread ที่มาประมวลผล โดยที่ก็จะวน ๆ ใช้จาก thread pool ไม่ใช่ว่าสร้างใหม่บ่อย ๆ มีอีก thread นึงคือไป accept request แล้วอ่านข้อมูลมารอใส่ queue ชื่อ todo ไว้ นอกจากนั้น parser ก็เขียนด้วย C ทำให้เร็วขึ้นไปอีก Puma นี่มันเลือกแตก process ผสมกับ Thread ได้ด้วย ผมเดาว่าเพราะ MRI-Ruby มันติด global lock ถ้าเจองานใช้ CPU มาก ๆ อาจจะใช้ได้ไม่ทั่วถึง แต่ถ้าไปใช้ JRuby แทนก็เป็นอันตกไป

Tokio เขียนบน Rust ที่มี mio ที่ไปหุ้ม epoll และ kqueue ได้เหมือน node.js ถ้าเทียบกันน่าจะอยู่ในระดับเดียวกับ libuv แต่ว่า Rust ไม่มีปัญหาเรื่องแตก thread อยู่แล้ว Tokio ก็เลยทำได้ทั้งแตก thread (multithread) แล้วก็มี event loop แบบ node.js ด้วย ในแต่ละ thread ก็มี event loop เองเลย

อีกวิธีหนึ่งของค่าย Rust คือทำ coroutine คล้าย ๆ Goroutine ขึ้นมาเลยใน project ชื่อ May แต่ทั้ง May และ Tokio นี่มันเป็นระดับล่างมาก ๆ ผมเข้าใจว่าคนทำเว็บส่วนมากคงไม่มาเรียกตรง ๆ น่าจะใช้ผ่าน Rocket หรือ Actix แทน

Rust นี่แตก thread กันเป็นว่าเล่นส่วนหนึ่งอาจจะเพราะว่ามันไม่มี GC ส่วนหนึ่งที่ยากเวลาแตก thread คือต้องทำ GC ให้ใช้งานสอดคล้องกับ scheduler ด้วย ผมเห็นสองภาษาที่โมเรื่องนี้อยู่คือ Ocaml และ Crystal ถ้าเสร็จแล้วก็น่าจะจัดอยู่กลุ่มเดียวกับ Golang ได้ แต่อาจจะเขียนง่ายกว่า