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
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à:
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.
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

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
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.
Bài viết liên quan
Bài viết mới