thảo luận Cùng học và chia sẻ kinh nghiệm Crystal

Hiện nay, trong các ngôn ngữ lập trình Web, Crystal đang nổi lên là một ngôn ngữ nhiều triển vọng. Với tiêu chí: cú pháp đẹp như Ruby, chương trình chạy nhanh như C, cộng thêm với các Web Framework hay như Lucky, Amber, Kemal thì anh em giờ hoàn toàn có thể xây dựng nhanh các trang web tiện dụng, hiệu năng cao! :)

Topic này lập ra để chia sẻ kiến thức, tài liệu cũng như thảo luận về ngôn ngữ Crystal. Mong các anh em cùng vào bàn luận cho xôm! :p
 

Mở bài​

Ngôn ngữ Ruby có cú pháp rất ngắn gọn, dễ đọc, dễ viết. Nó là ngôn ngữ thông dịch, kiểu động và hướng đến tính tiện lợi cho lập trình viên lên hàng đầu. Nhưng những điều này phải đánh đổi bằng một phần hiệu năng của ngôn ngữ. Nếu so với các ngôn ngữ bạn bè cùng trang lứa, Ruby không hề là một ngôn ngữ có điểm mạnh về hiệu năng. Nó vẫn "đủ nhanh" nếu dùng cho phát triển Web backend, nơi cần tốc độ phát triển sớm nhất có thể và hiệu năng ngôn ngữ không phải là yếu tố quan trọng hàng đầu.

Nhưng... Hãy thử tưởng tượng bạn muốn xây dựng một dự án Game rất "khủng" bằng Ruby. Việc sử dụng một ngôn ngữ không tận dụng hiệu quả không gian bộ nhớ và lãng phí nhiều chu kỳ CPU chắc chắn sẽ làm game của bạn render được ít đa giác hơn và cho số FPS thấp hơn rất đáng kể.

Vậy liệu có ngôn ngữ nào vượt trội về hiệu năng trong khi vẫn giữ được cú pháp quen thuộc như khi bạn dùng Ruby hay không? Câu trả lời rất có thể là Crystal.

Giới thiệu Crystal​


Crystal


Nói ngắn gọn nhất, Crystal là ngôn ngữ biên dịch, kiểu tĩnh, hướng đến hiệu năng trong khi vẫn giữ cú pháp thật quen thuộc với Ruby. Dưới đây, mình xin chia sẻ một vài đặc trưng cơ bản nhất của Crystal.

Cú pháp​

Cú pháp của Crystal gần như y hệt Ruby. Tuy nhiên Crystal không hướng đến tương thích ngược với Ruby:

# A very basic HTTP server
require "http/server"

server = HTTP::Server.new do |context|
context.response.content_type = "text/plain"
context.response.print "Hello world, got #{context.request.path}!"
end

puts "Listening on http://127.0.0.1:8080"
server.listen(8080)

Kiểu dữ liệu​

Nếu kiểu dữ liệu đã hiển nhiên thì bộ biên dịch của Crystal có thể tự suy ra kiểu dữ liệu từ đoạn code trong quá trình biên dịch. Nhờ vậy, trong đa số trường hợp, bạn có thể bỏ qua chỉ rõ kiểu dữ liệu và thoải mái viết code như mọi khi bạn viết bằng Ruby/Python.

def shout(x)
# Notice that both Int32 and String respond_to to_s
x.to_s.upcase
end

foo = ENV["FOO"]? || 10

typeof(foo) # => (Int32 | String)
typeof(shout(foo)) # => String

Chạy đồng thời nhiều tác vụ​

Crystal hỗ trợ chạy đồng thời nhiều tác vụ (concurrency) với Fiber. Khái niệm Fiber của Crystal rất giống với Goroutine trong Golang. Mỗi Fiber là một dạng green thread, khá giống với OS thread nhưng rất nhỏ nhẹ. Điều này giúp bạn có thể tạo ra một lượng khủng Fiber (vài triệu cái chẳng hạn) mà không tốn mấy bộ nhớ. Mọi Fiber đều chạy trên cùng một OS thread, như vậy chúng không chạy song song cùng thời điểm (parallelism). Các Fiber có thể giao tiếp với nhau theo bằng cơ chế channel (khác với sử dụng vùng bộ nhớ chung như OS thread).

Fiber không bao giờ bị dừng thực thi giữa chừng như OS thread (pre-emptive). Chỉ khi nào một Fiber đang chạy bị block ở một tác vụ nào đó hoặc bản thân Fiber đang chạy cho phép, hệ thống lên lịch mới chuyển sang chạy Fiber khác. VÍ dụ một vài trường hợp bị Fiber được chuyển là:
  • Đợi một tác vụ I/O nào đó hoàn thành
  • Đợi client nhận dữ liệu
  • Sử dụng phương thức sleep
  • Hoặc nếu bản thân Fiber cho phép với class method Fiber.yield
Ở đây, mình tạo 2 fiber và sử dụng sleep để sau mỗi lần in ra màn hình, Fiber sẽ ngủ một thời gian ngẫu nhiên.

channel = Channel(Nil).new

spawn do
(0..10).each do |n|
puts "Tam: #{n}"
sleep Random.rand(3000).milliseconds
end
channel.send(nil)
end

spawn do
(0..10).each do |n|
puts "Cam: #{n}"
sleep Random.rand(3000).milliseconds
end
channel.send(nil)
end

channel.receive
channel.receive

Dưới đây là kết quả của đoạn code trên:

$ crystal run app.cr
Tam: 0
Cam: 0
Cam: 1
Tam: 1
Cam: 2
Tam: 2
Cam: 3
Tam: 3
Cam: 4
Cam: 5
Tam: 4
Cam: 6
Cam: 7
Cam: 8
Tam: 5
Cam: 9
Tam: 6
Tam: 7
Cam: 10
Tam: 8
Tam: 9
Tam: 10

Có thể dễ thấy rằng 2 fiber mình tạo ra đang được thực thi cùng lúc (concurrency). Khi thực thi 2 fiber này, mỗi khi một fiber bị chặn thực thi bởi hàm block, hệ thống đặt lịch sẽ chuyển sang thực thi fiber kia. Như vậy, mặc dù chúng được thực thi đồng thời (concurrency) nhưng không phải là song song (parallel). Mình cũng dùng thêm channel để giúp fiber chính chờ cho đến khi nào 2 fiber con của mình đã chạy xong. Nếu không sử dụng channel, chương trình trên của mình sẽ bị đóng lại ngay lập tức.

Bản mới nhất của Crystal (0.31.0) mới được ra mắt khoảng 1 tuần trước (tính ở thời điểm publish bài) đã hỗ trợ multi threading thử nghiệm. Điều này tức là những fiber có thể được phân bổ qua nhiều OS thread khác nhau và đạt được parallel thực thụ 😍 Nhưng cũng có nghĩa là bạn sẽ phải tuân thủ sử dụng channel để giao tiếp với các Fiber, nhằm tránh bị condition race.

Hiểu thêm về Fiber và concurrency tại https://crystal-lang.org/reference/guides/concurrency.html

Macros​

Một trong những điểm mạnh nhất của Ruby so với mọi ngôn ngữ khác là meta-programming, thứ giúp những framework với cú pháp ngắn gọn như Rails trở thành hiện thực. Câu trả lời của Crystal cho điều này là tính năng macro.

macro define_method(name, content)
def {{name}}
{{content}}
end
end

# This generates:
#
# def foo
# 1
# end
define_method foo, 1

foo #=> 1

Ruby có gems, Crystal có shards 👍

Các package của Crystal được gọi là shard.

Để sử dụng một shard nào đó trong project, ví dụ như shard kemal chẳng hạn, thì ở thư mục gốc của project, bạn chỉ cần tạo thêm file shard.yml như thế này:

dependencies:
kemal:
github: kemalcr/kemal

Sau đó bạn chỉ cần chạy lệnh shard install là xong. Lệnh shard cũng được đi kèm luôn với gói phân phối của Crystal.

Link nguồn bài viết:
https://viblo.asia/p/thich-cu-phap-...thap-hay-thu-ngon-ngu-moi-crystal-m68Z0xJ9ZkG
 

soihoang

Member

... Mọi Fiber đều chạy trên cùng một OS thread, như vậy chúng không chạy song song cùng thời điểm (parallelism). Các Fiber có thể giao tiếp với nhau theo bằng cơ chế channel (khác với sử dụng vùng bộ nhớ chung như OS thread).


Mình đang quan tâm tới chổ concurrency và parallelism. Mình đọc tới chổ này cũng ko hiểu là sao các fiber đều chạy trên 1 thread được ?

Theo mình hiểu fiber là 1 dạng green thread độc lập, sẽ được scheduler assign cho 1 worker là 1 os thread thật. Khi nào nó yield thì sẽ nhả worker đó ra để trả về worker pool cho fiber nào cần hoặc.

Nếu cùng chạy trên 1 thread thì có trường hợp nào lỡ dev nó quên yeild hay xử lý cpu intensive nó block hết cả hệ thống không.

Mặc định crystal set worker là 4, thì nếu chạy trên server cả chục core thì tận dụng cpu như thế nào.
 
Last edited:
Mình đang quan tâm tới chổ concurrency và parallelism. Mình đọc tới chổ này cũng ko hiểu là sao các fiber đều chạy trên 1 thread được ?

Theo mình hiểu fiber là 1 dạng green thread độc lập, sẽ được 1 scheduler asign cho 1 worker là 1 thread os thật. Khi nào nó yield thì sẽ nhả worker đó ra cho fiber nào cần.

Nếu cùng chạy trên 1 thread thì có trường hợp nào lỡ dev nó quên yeild hay xử lý cpu intensive nó block hết cả hệ thống không.

Mặc định crystal set worker là 4, thì nếu chạy trên server cả chục core thì tận dụng cpu như thế nào.

Anh có thể đọc trên trang chủ của Crystal về concurrency :)
https://crystal-lang.org/reference/guides/concurrency.html
 

Nipin

Senior Member
Mình đang quan tâm tới chổ concurrency và parallelism. Mình đọc tới chổ này cũng ko hiểu là sao các fiber đều chạy trên 1 thread được ?

Theo mình hiểu fiber là 1 dạng green thread độc lập, sẽ được scheduler assign cho 1 worker là 1 os thread thật. Khi nào nó yield thì sẽ nhả worker đó ra để trả về worker pool cho fiber nào cần hoặc.

Nếu cùng chạy trên 1 thread thì có trường hợp nào lỡ dev nó quên yeild hay xử lý cpu intensive nó block hết cả hệ thống không.

Mặc định crystal set worker là 4, thì nếu chạy trên server cả chục core thì tận dụng cpu như thế nào.
có event loop mà. xem cái issue này chẳng hạn: https://github.com/crystal-lang/crystal/issues/6957

còn vụ parallel thì giờ nó vẫn chưa ngon hẳn đâu, trên 4 threads là performance drop rồi. crystal vừa muốn concurrent vừa muốn parallel lại dính global variables với mutable state, muốn ngon hẳn chắc cũng mất vài năm nỗ lực nữa.

mà thực ra thì spawn nhiều cái process, reuse port giống nodejs bây giờ cũng ổn mà đâu cần phải cố dùng hết core đâu.

btw, concurrent với crystal dễ vãi, ví dụ tôi muốn throttle cái crawler chỉ hit 20 phát cùng lúc thì đơn giản có một đoạn:

Ruby:
ch = Channel(Nil).new(20)

queue.each_with_index(1) do |data, idx|
  spawn do
    do_io_task(data)
  ensure
    ch.send(nil)
  end
  
  ch.receive if idx > 20
end

20.times { ch.receive }
là xong, trải nghiệm chưa bao giờ đơn giản hơn thế.
 
Last edited:

soihoang

Member
crystal đang bị chê là compilation chậm, mấy bác sửa 1 dòng code của 1 code base nhỏ tầm side project thì thấy nó có lâu ko(tầm chục s).
 

Nipin

Senior Member
Tôi compile thì không chậm, anh @Nipin có gặp trường hợp này không?
cái project chivi (dùng kemal) của tôi src/ tầm 4k+ line of code, dùng sentry để reload lúc dev mất tầm 3~5s. nếu code full webdev project (có cả template) thì chắc cũng khó chịu nhưng tôi chỉ là code api cho nên cũng chả thấy vấn đề lắm, vì hay gặp lỗi typo/type thì nó catch gần như instantly.

cái crystal chậm nhất là do vụ không có incremental compilation, tôi lần nào build prod release cũng phải mất từ 30s tới 1 phút tuỳ script, mấy project to mấy chục nghìn dòng code thì chắc mỗi lần sửa phải mất vài chục phút compile.

p/s: spec thì chạy nhanh như gió, rất kỳ dị. cơ mà tôi cũng lười viết spec, dùng exunit của elixir quen rồi quay sang mấy thằng kiểu it với eq thấy khó chịu vãi.

p/p/s: nói 5s reload nghe to vãi nhưng nhớ ngày xưa trải nghiệm react + typescript cũng có khác quái gì :))
 
Last edited:
cái project chivi (dùng kemal) của tôi src/ tầm 4k+ line of code, dùng sentry để reload lúc dev mất tầm 3~5s. nếu code full webdev project (có cả template) thì chắc cũng khó chịu nhưng tôi chỉ là code api cho nên cũng chả thấy vấn đề lắm, vì hay gặp lỗi typo/type thì nó catch gần như instantly.

cái crystal chậm nhất là do vụ không có incremental compilation, tôi lần nào build prod release cũng phải mất từ 30s tới 1 phút tuỳ script, mấy project to mấy chục nghìn dòng code thì chắc mỗi lần sửa phải mất vài chục phút compile.

p/s: spec thì chạy nhanh như gió, rất kỳ dị. cơ mà tôi cũng lười viết spec, dùng exunit của elixir quen rồi quay sang mấy thằng kiểu it với eq thấy khó chịu vãi.

p/p/s: nói 5s reload nghe to vãi nhưng nhớ ngày xưa trải nghiệm react + typescript cũng có khác quái gì :))

Anh nói thế là rất rõ rồi! :)
Tôi không thấy chậm bởi tôi đã từng làm React, Typescript thì trải nghiệm chả khác gì như anh nói! Nếu là Kotlin/Java hay .NET thì compile còn chậm hơn! :)
 
Top