Khủng hoảng đa lõi: Scala vs Erlang
Cuộc tranh luận nóng bỏng về Scala vs. Erlang qua các trang blog gần đây càng nhiều. Tương lai sẽ là đa lõi, nhưng câu hỏi đó là khủng hoảng đa lõi sẽ được giải quyết thế nào. Scala và Erlang là hai ngôn ngữ có khả năng trở thành lời giải nhất, nhưng chúng hơi khác nhau. Cùng Bizfly Cloud tìm hiểu ưu và khuyết điểm của chúng là gì?
Năm 2008, Erlang, Clojure, và Scala bắt đầu nổi (nhưng chưa nổi rõ như bây giờ). Mình khủng hoảng không biết nên chọn học thằng nào, rốt cục học luôn cả ba (theo thứ tự trên). Hiện mình thích nhất Erlang vì cú pháp nó đơn giản dễ học (còn dễ hơn JavaScript, nhưng học xong còn thông minh ra, chứ không chỉ có mỗi biết thêm một bộ cú pháp ngôn ngữ mới) và dễ scale ra nhiều máy. Còn trong đa số project thì mình dùng Scala vì ngôn ngữ thân thiện như Ruby và Erlang, có thể dùng rất nhiều thư viện Java và Scala có sẵn (không dùng thì... ngu). Clojure thì mình thấy khó dùng.
Vấn đề
Định luật Moore đã thay đổi. Chúng ta không thể tăng xung nhịp CPU lên mãi như trước được. Thay vào đó, chúng ta có nhiều lõi hơn. Hiện nay, ngay cả máy xách tay của bạn cũng có thể có hai lõi.
Để dùng được hơn một lõi, ứng dụng của bạn phải đa nhiệm. Nếu khách hàng của bạn mua máy có 8 lõi, bạn sẽ khó ăn nói khi giải thích cho họ là thường chương trình chỉ dùng được khoảng 12% khả năng của CPU, ngay cả nếu máy chỉ dùng để chạy mỗi chương trình đó.
Trong tương lai, điều này còn tồi tệ hơn. Không những chương trình đơn nhiệm của bạn không chạy nhanh hơn, mà thật ra nó còn chạy chậm hơn. Lí do là bạn càng có nhiều lõi, thì mỗi lõi sẽ chạy chậm hơn vì lí do năng lượng và nhiệt. Máy tính và điện thoại có nhiều core bây giờ là chuyện rất bình thường, và xu hướng của công nghệ cho thấy trong lúc bạn chưa nhận ra thì số lõi sẽ lên đến hàng ngàn. Nhưng mỗi lõi sẽ chạy chậm hơn so với lõi hiện nay.
Mã đa nhiệm
Vấn đề hiển nhiên để giải bài toán là viết (và viết lại) phần mềm thành đa nhiệm. Cách phổ biến nhất là dùng luồng (thread), nhưng hầu hết lập trình viên cho rằng ứng dụng đa luồng rất khó viết. Treo (deadlock), đói (starvation) và tình trạng đua (race condition) là những khái niệm quá quen thuộc với đa số lập trình viên lập trình đa nhiệm. Cả Erlang và Scala đều giúp giải thoát nỗi khổ đó.
Sơ lược về 2 ngôn ngữ
Scala còn thỉnh thoảng được coi là ngôn ngữ mới và nổi nhất được chạy trên JVM. Nó kết hợp lập trình hướng đối tượng và lập trình hàm, có cú pháp súc tích hơn Java, định kiểu tĩnh và nhanh, thậm chí thỉnh thoảng còn nhanh hơn cả JavaScript. Có nhiều lí do để tìm hiểu Scala cẩn thận.
Erlang là ngôn ngữ được thiết kế với mục đích chịu được "va đập", nhưng nhờ thiết kế đó, nó là ngôn ngữ dễ scale. Nó ra đời trước Java, nhưng lại thường được coi là ngôn ngữ của tương lai đa nhiệm. Nó là ngôn ngữ định kiểu động, hàm, và từng được dùng để viết ra hệ thống hầu như không bao giờ down.
Cuộc tranh luận
Thế thì cuộc tranh luận Scala vs. Erlang là về cái gì? Cốt lõi là tốc độ và khả năng scale, nhưng cuộc tranh luận cũng bao gồm nhiều thứ khác như phong cách, tính năng ngôn ngữ và thư viện hỗ trợ. Cuộc tranh luận bắt đầu một cách không chủ đích khi Ted Neward đưa ra ý kiến về vài ngôn ngữ và nói rằng "việc Erlang có chương trình thông dịch riêng là xấu".
Steve Vinoski và Ted từ đó đã tranh luận vài lần, nhưng cuộc thảo luận sau đó chuyển sang vài blog khác nơi nó nhấn mạnh những khác biệt và tương đồng thú vị giữa Scala và Erlang. Chúng tôi sẽ tổng kết từng điểm thú vị để nêu ra ưu và khuyết của từng ngôn ngữ và nhiều quan điểm khác nhau về vài vấn đề.
Độ tin cậy
Steve Vinoski trả lời post của Ted, nói về việc Erlang chạy trên chương trình thông dịch riêng.
Việc nó chạy trên chương trình thông dịch riêng của nó là điều tốt; nếu không nó không có đáng tin cậy và sẽ chỉ là ngôn ngữ kì quái mới mang tính thử nghiệm.
Steve nói rằng ngay cả khi ngôn ngữ đáng tin cậy, thì cái dùng để chạy nó cũng phải đáng tin cậy. Vì Erlang ngay từ đầu đã được thiết kế để đáng tin cậ và đa nhiệm, nó không gặp vấn đề thường phát sinh khi chuyển sang đa nhiệm, thường là thư viện bên dưới phải chạy tốt trong chế độ đa nhiệm.
Scala chạy trên JVM và điểm đáng chú ý là chương trình Scala có thể dùng mã Java có sẵn. Tuy nhiên, rất nhiều mã Java không được thiết kế cho đa nhiệm, nên mã Scala cần tính đến vấn đề này.
Tiến trình hạng nhẹ (lightweight process)
Để chạy rất nhiều ứng dụng đa nhiệm, cần chạy cùng lúc nhiều thứ. Có vài cách thực hiện việc này. Dùng luồng là cách phổ biến, dùng tiến trình là một cáh nữa. Điểm khác nhau là luồng dùng chung không gian bộ nhớ với nhau, còn tiến trình khác nhau thì không dùng chung gì cả. Điều đó có nghĩa luồng cần cơ chế lock như mutex để ngăn hai luồng thao tác cùng bộ nhớ cùng lúc, nhưng tiến trình thì không gặp vấn đề đó và thay vì thế nó dùng cách truyền thông điệp để liên lạc với nhau. Nhưng thường tiến trình khá "tốn tiền" khi xét đến tốc độ tạo và bộ nhớ chiếm dụng, và đó là một lí do người ta thường chọn luồng để thực hiện đa nhiệm, ngay cả khi khó lập trình.
Steve Vinoski viết:
Khả năng đa nhiệm trở nên dễ dàng khi có cấu trúc cung cấp tiến trình nhẹ, nhưng điều đó không có nghĩa rằng một khi thiết kế xong cấu trúc này, vấn đề còn lại không chỉ đơn giản là bắt tay viết chương trình.
Erlang theo hướng tiếp cận này. Tiến trình Erlang rất nhẹ, và một chương trình Erlang thường có vài chục ngàn cái.
Scala mặt khác thực hiện việc tương tự bằng actor dựa trên sự kiện (event-based actor). Yariv Sadan giải thích cách làm:
Scala có hai loại Actor: dựa trên luồng và dựa trên sự kiện. Actor dựa trên luồng chạy trong luồng hạng nặng của hệ điều hành. Chúng chạy song song với nhau, nhưng không thể mở rộng ra hơn vài ngàn actor trên một máy ảo. Actor dựa trên sự kiện là đối tượng bình thường. Chúng rất nhẹ cân, và, như tiến trình của Erlang, có thể tạo ra hàng triệu cái trên máy tính cấu hình bình thường.
Tuy nhiên cũng có điểm khác biệt, Yariv giải thích:
Điểm khác với tiến trình của Erlang là trong mỗi luồng của hệ điều hành, các actor dựa trên sự kiện chạy tuần tự (sequentially) và không có cái nào được ưu tiên hơn cái nào. Điều này dẫn đến việc một actor dựa trên sự kiện có thể chiếm dụng toàn bộ luồng hệ điều hành mà trong đó nó đang chạy (có thể không đoán trước được).
Tính không thể thay đổi
Erlang là ngôn ngữ hàm. Điều này có nghĩa dữ liệu không thay đổi, như chuỗi của Java, nên không xảy ra hiệu ứng phụ. Bất kì thao tác trên dữ liệu nào cũng đều phát sinh phiên bản mới của dữ liệu, nhưng cái cũ vẫn y nguyên. Tính không thể thay đổi là yếu tố rất quan trọng tạo nên tính an toàn yển chuyển, vì mã chương trình không thể thay đổi dữ liệu gốc (cái có thể được dùng chung). Nếu dữ liệu không thay đổi, nguy cơ nó bị thay đổi bởi hai cái đang chạy song song là không tồn tại và hơn thế dữ liệu có thể được sao chép sang máy tính khác mà không cần đồng bộ hoá.
Vì Scala dựa trên JVM và kết hợp lập trình hướng đối tượng là lập trình hàm, không có gì đảm bảo tính không thể thay đổi nếu so sánh với ngôn ngữ hàm. Tuy nhiên, ở phần bình luận của bài của Yariv, có cuộc thảo luận thú vị giữa Yariv và David Pollack về sự khác nhau của hai loại ngôn ngữ. David, tác giả của framework Lift viết bằng Scala, nêu quan điểm về tính không thể thay đổi.
Tính không thể thay đổi — Erlang ép lập trình viên phải tuân theo, và hầu như không thể tránh khỏi khi dùng Erlang. Nhưng như vậy thì bạn phải đánh đổi những tính năng tuyệt vời khác của Scala lấy sự ép buộc này. Tôi dùng kiểu dữ liệu không thể thay đổi khi lập trình actor với Scala.
Yariv hỏi:
Dùng mỗi một kiểu dữ liệu không thể thay đổi có phải là bị hạn chế quá không? Ví dụ anh không thể lấy bean từ Hiberate và gửi nó cho actor khác.
David trả lời:
Tôi từng làm nhiều sản phẩm bán cho khách dựa trên Scala actor. Thực tế chỉ phải viết thêm tí mã để giải quyết vấn đề dữ liệu không thể thay đổi này. Chỉ phải định nghĩa lớp để chứa thông điệp là không thể thay đổi là xong.
Hệ thống định kiểu
Erlang được định kiểu động. Scala thì được định kiểu tĩnh và có hệ thống định kiểu mạnh hơn Java. Tuy nhiên, điểm khác biệt lớn so với Java là Scala có tính năng tự suy ra kiểu. Điều này có nghĩa bạn có thể loại bỏ được rất đoạn mã khai báo kiểu dữ liệu, mã sẽ sạch sẽ hơn nhưng trình biên dịch vẫn phải kiểm tra kiểu.
Tranh luận về ưu nhược của ngôn ngữ động và tĩnh chắc chẳng bao giờ kết thúc, nhưng đó là điểm khác biệt cần chú ý giữa Erlang và Scala.
Đệ qui đuôi (tail recursion) và vòng lặp
Lại Yariv:
Lập trình hàm và đệ qui đi đôi với nhau. Thực tế, khó có thể viết chương trình Erlang mà không dùng đệ qui đuôi vì Erlang không có vòng lặp — nó dùng đệ qui cho mọi thứ (tôi nghĩ đây là cái hay).
Chắc chắn đây là điểm làm cho Erlang khác nhiều so với Scala. Scala có vòng lặp truyền thống, nhưng David Pollack chẳng thấy đệ qui đuôi có lợi điểm gì khi nói:
Đệ qui đuôi — Chẳng thành vấn đề cho actor dựa trên sự kiện.
Trong trường hợp này, vấn đề chỉ là ý thích và phong cách lập trình của lập trình viên.
Cập nhật nóng mã chương trình
Vì Erlang được thiết kế vì độ tin cậy cao, cập nhật nóng mã chương trình (thay đổi mã khi chương trình đang chạy) là tính năng có sẵn trong ngôn ngữ.
JVM cũng hỗ trợ chút ít tính năng này. Lớp có thể thay đổi, nhưng vì hệ thống định kiểu tĩnh, danh sách tham số của hàm không thể thay đổi - chỉ thân hàm. Có thư viện để khắc phục điều này, có framework cho phép làm điều này. Jonas Bonér có ví dụ cách thực hiện với Scala actor.
Tổng kết
Cả Scala và Erlang đều là ngôn ngữ hướng đến giải quyết khủng hoảng đa lõi. Chúng có nguồn gốc khác nhau và tuổi tác khác nhau nên có hướng tiếp cận khác nhau đối với một số vấn đề, nhưng chúng giống nhau nhiều hơn là khác nhau, ít nhất đối với vấn đề đa nhiệm.
Erlang đã được dùng vài thập kỉ, và trong thực tiễn đã chứng tỏ bản lĩnh trong nhiều hệ thống lớn. Một trong những hạn chế là ít người biết, tuy nhiên độ phổ biến cũng không ảnh ảnh cộng đồng Erlang mấy.
Scala mặt khác còn là đứa trẻ trong lãnh vực của Erlang. Cũng có vài sản phẩm được được đưa vào sử dụng, cũng có công ty đánh cược tương lai cho nó. Lợi thế lớn nhất của Scala so với Erlang là nó chạy trên JVM và có thể sử dụng mã và công cụ Java có sẵn. Tuy vậy, hầu hết mã Java không tương thích với mô hình actor của Scala.
Cả hai ngôn ngữ cung cấp cách giải quyết giống nhau cho vấn đề có áp lực ngày càng lớn mà những ngôn ngữ khác khó giải quyết. Hi vọng bạn đã hiểu rõ hơn tình hình để có thể chọn học một ngôn ngữ để giải quyết vấn đề của mình sau khi đọc xong bài tổng kết này.
Tương lai là đa lõi. Scala và Erlang sẽ phổ biến.
>> Tìm hiểu thêm: Scala: Trait và lập trình hướng đối tượng nâng cao