Với kiểu lỗi error là loại lỗi tạo ra do quá trình chạy vì vậy nguyên nhân của lỗi sẽ được Erlang sinh ra, nguyên nhân của lỗi có thể là một trong cách loại sau:
Nguyên nhân Giải thích
badarg Tham số vào bị sai kiểu hoặc dạng đưa vào không đúng. badarith Tham số vào sai trong một phép toán số học.
{badmatch,V} Tính toán biểu thức không thành công.
function_clause Không có mẫu nào phù hợp với lời gọi hàm được đưa ra. {case_clause,V} Không nhánh nào trong case expression phù hợp
if_clause Không có rẽ nhánh nào phù hợp
{try_clause,V} Kết quả tính được sau lệnh try không phù hợp với các vế sau đó undef Không thể tìm hàm tương ứng với lời gọi
{badfun,F} Hàm F bị lỗi
{badarity,F} Số lượng các tham số của hàm F không phù hợp
timeout_value Thời gian time_out của receive…after không phải là một số nguyên hay là vô cùng.
noproc Link đến một process không tồn tại
{nocatch,V} Cố gắng thực hiện throw khi không nằm trong catch expression system_limit Vượt giới hạn của hệ thống
MÔI TRƯỜNG THỰC THI CỦA ERLANG
L. Concurrent Programming
Thành phần chính của lập trình tương tranh trong ngôn ngữ Erlang là các process. Các process được xây dựng như là những máy ảo riêng biệt để thực hiện các hàm của Erlang. Chính vì vậy với lập trình bằng ngôn ngữ Erlang thì các process do ngôn ngữ tạo ra chứ không phụ thuộc vào hệ điều hành.
Với mỗi process trong Erlang thì: • Việc tạo và hủy process rất nhanh.
• Việc gửi các message giữa các process cũng xảy ra rất nhanh.
• Các process như nhau khi thực hiện trên các hệ điều hành khác nhau. • Lượng process được tạo ra có thể rất lớn.
• Cách duy nhất để các process liên lạc với nhau đó là thực hiện thông qua truyền và nhận các message.
Trước khi đi sâu hơn về các process ta nói thêm về các lập trình tương tranh này. Trong thế giới thực mọi công việc để tăng hiệu quả đều cần được thực hiện song song với phương pháp lập trình tương tranh, các tiến trình xử lý đồng thời và song song các công việc vì vậy sẽ làm tăng hiệu quả của việc thực hiện các yêu cầu cũng như xử lý tinh toán nhờ việc thực hiện song song chúng.
Với Erlang các process không thực hiện chia sẻ chung bộ nhớ mà thực hiện tương tác với nhau thông qua các thông điệp được truyền qua lại giữa các process. Cách làm này khá giống trong tự nhiên đó là khi các công việc được chia cho các cá nhân thực hiện thì mỗi cá nhân (ở đây muốn nói đến con người) không thể có chung bộ nhớ được, mỗi người có bộ nhớ riêng, cách nhanh nhất để họ có thể tương tác với nhau đó là liên lạc bằng cách gửi các thông tin qua lại từ đó các tương tác được thực hiện.
Với Erlang các process rất nhỏ và không chia sẻ bộ nhớ vì vậy không xảy ra hiện tượng các process có thể tự khóa lần nhau, chờ đợi nhau do việc dùng chung bộ nhớ. Các process có thể thực hiện song song hóa rất lớn do không phải chờ đợi nhau vì vậy tốc độ thực hiện công việc được tăng lên đáng kể. Erlang lại thực hiện theo paradigm là function programming vì vậy nó sẽ không gây ra các side effect có thể ảnh hưởng đến kết quả khi thực hiện song song và không cần các cơ chế để kiểm tra các side effect này khi thực hiện song song.
Khi xảy ra hiện tượng các process bị crash, cách ứng xử của các process cũng sẽ giống với các cá nhân thực hiện các công việc đó là các process trước khi exit sẽ thực hiện thông bác đến các process được liên kết với nó thông tin về việc exit cùng với nguyên nhân của việc exit này. Từ đó các process tương ứng này sẽ xác định việc phải làm khi process khác bị crash.
Các process trong Erlang nhỏ và rất dễ để gọi, hủy bỏ cũng như liên lạc giữa chúng cũng khá đơn giản vì vậy khi cần tăng tốc độ hoặc cần thực hiện công việc mới ta có thể gọi thêm process mà không lo các yêu cầu về tài nguyên bộ nhớ cũng như việc quản lý các process.
M. Process trong Erlang
Để thực hiện các thao tác làm việc cũng như thực thi các ứng dụng thông qua các process ta tìm hiểu về cách gọi cũng như các thao tác thực hiện với process. Có ba thao tác cơ sở với process đó là spawn, send, receive :
• Send là việc gửi một message đến một process.
• Receive là các đáp ứng của một process khi các message được chuyển đến. Để thực hiện quá trình Spawn ta chỉ cần thực hiện lời gọi hàm sau :
Pid = spawn(Fun)
Khi đó hàm spawn sẽ tạo ra một process và trả về kết quả là con trỏ đến process thực hiện hàm Fun. Các gọi hàm đầy đủ như sau cùng với các yêu cầu về các tham số đầu vào :
spawn(Module, Name, Args) -> pid() Module = Name = atom()
Args = [Arg1,...,ArgN] ArgI = term()
Ngoài ra còn rất nhiều các hàm BIF spawn khác với số lượng các tham số khác nhau để thực hiện tạo ra một process.
Với quá trình Send ta chỉ cần thực hiện phép toán rất đơn giản với toán tử “!”. Muốn gửi một Message đến một process (Pid) ta thực thiện phép toán:
Pid ! Message.
Để gửi đến nhiều process khác nhau với cùng một Message ta có một cách viết gọn là
Pid1 ! Pid2 ! ... ! M
Khi các tin nhắn đến các process ta thực hiện Receive các tin nhắn thông qua expression sau:
receive
Pattern1 [when GuardSeq1] -> Body1;
...;
PatternN [when GuardSeqN] -> BodyN
end
Các tin nhắn sẽ lần lượt được so với các mẫu Pattern1, … PatternN nếu phù hợp với mẫu nào sẽ kiểm tra GuardSeq tương ứng nếu phù hợp sẽ thực hiện Body tương ứng.
Bên cạnh mẫu Receive như trên người ta còn có mẫu receive … after:
Trong đó ExprT là một số nguyên (integer), giá trị lớn nhất của nó được phép là 16#FFFFFFFF
Có một cách rất hay để tạo nên mức ưu tiên với các message được gửi đến đó là tạo các khối receive lồng trong after 0 như sau:
receive
Pattern1 [when GuardSeq1] -> Body1;
...;
PatternN [when GuardSeqN] -> BodyN
after 0 ->
receive
PatternExt1 [when GuardSeq1] -> Body1;
...;
PatternExtN [when GuardSeqN] -> BodyN
end
Tuy nhiên không nên quá lạm dụng cách làm này với số lượng message lớn.
Ngoài các phép cơ bản trên ta còn có cách phép toán khác khi thực hiện với các process:
• Đăng ký - register: Bên cạnh việc đánh địa chỉ cho các process thông qua các Pid, ta cũng được cung cấp những BIF riêng dành cho việc đăng ký một tên cho một process quá trình này được gọi là register. Mỗi tên được đăng ký cho một process phải là một atom, và khi process kết thúc tên này sẽ tự động được hủy đăng ký
unregister. Dưới đây là bảng cho thấy các hàm thực hiện với phép toán:
Các phép toán Giải thích
register(Name, Pid) Đăng ký tên Name cho Pid
register() Đưa ra một List các Pid đã được đăng ký
whereis(Name) Trá lại kết quả là một Pid nếu Name đã
được đăng ký cho Pid, trả lại kết quả là undefined nếu như chưa có Pid nào đăng ký với tên là Name.
unregister(Name) Hủy đăng ký của Name
• Từ điển của process: Mỗi process có một từ điển riêng từ điển này cho phép ta có thể ghi giá trị vào phục vụ khá hiệu quả trong việc lập trình các hàm sau dùng để thực hiện các quá trình cập nhật trên từ điển này:
put(Key, Value) get(Key) get() get_keys(Value) erase(Key) erase()
N. Exception Handling với các Process
Với các exception handling thông thường đã được trình bày ở phần trước với cách viết ứng dụng một cách tuần tự. Trong một process đơn thì việc xử lý các ngoại lệ cũng tương tự như vậy. Nhưng với một hệ thống các process thì việc quản lý với các ngoại lệ được một cách đặc biệt.
Các process thông thường khi gặp lỗi sẽ tự động kết thúc nếu không có một cách quản lý hiệu quá các ứng dụng sẽ không đạt được kết quả cuối cùng và rất khó để biết được lý do process bị gặp lỗi. Để xử lý được điều này Erlang xử lý giống như đã nói ở trên, khi một process kết thúc do gặp lỗi (die) nó sẽ gửi những thông tin này đến các process có quan hệ với nó để các process này có thể có được những thông tin về lý do của process đã bị kết thúc do gặp lỗi.
Để tạo kết nối giữa hai process Erlang cung cấp phép toán link giữa các process. Muốn tạo kết nối process A với process B thì trong phần thân của A cần có lời gọi link(PidB) hoặc ngược lại trong phần thân của B có lời gọi link(PidA). Đây là một kết nối 2 chiều tức nếu A được link đến B thì B cũng đã được link đến A theo lời gọi.
Tập hợp các process đã link đến process B được gọi là link set (tập link) của process B.
Nếu vì một lý do nào đó mà process B bị kết thúc do lỗi nó sẽ gửi một thông điệp thông báo đến tất cả các process trong tập link set của mình trước khi thật sự kết thúc. Khi một thông điệp thông báo kết thúc do lỗi được gửi đến một process thông thường thì process này cũng tự động báo lỗi và kết thúc đến cho các process nằm trong tập link set của mình. Như vậy sẽ khó để lưu lại thông tin lỗi của các process để làm được điều này ta thực hiện theo hai cách sau:
a. Biến một process thông thường thành system process, đây là một process đặc biệt khi các tín hiệu thông báo kết thúc do lỗi được truyền đến nó sẽ được đưa vào mail box của process này với định dạng như sau: {'EXIT', Pid, Why} và process này sẽ thực hiện xử lý message này như các message thông thường khác. Để thực hiện được điều này ta thực hiện lời gọi hàm BIF, ví dụ muốn biến process PidA thành system process thì trong thân của process PidA cần phải có lời gọi
process_flag(trap_exit, true).
b. Tạo một monitor, đây là một trường hợp kết nối đặc biết khác với link. Để tạo được loại kết nối này ta thực hiện lời gọi hàm BIF, ví dụ muốn tạo monitor từ Pid1 đến Pid2 thì trong thân của Pid1 phải có lời gọi erlang:monitor(process, Pid2) hàm này sẽ trả về một reference.
Khi đó nếu Pid2 bị kết thúc do lỗi nguyên nhân là Reason thì một message ‘DOWN’ sẽ được đưa đến mail box của Pid1 với cấu trúc như sau: {'DOWN', Ref, process, Pid2, Reason}
Monitor là một quan hệ một chiều khác với link. Để hủy một monitor đến một Pid ta thực hiện lời gọi hàm erlang:demonitor(Ref).
Như vậy với mỗi process bất kỳ ta có thể có một bảng như sau:
Trap_exit Exit
Signal Hành động
true kill Kết thúc và gửi thông điệp thoát do lỗi cùng với nguyên nhân chocác process trong link set của mình true X Không kết thúc, nhận được tin nhắn thông báo vào mail box vàxử lý thông tin này như đã được lập trình false normal Không kết thúc tiếp tục chạy
false kill Kết thúc và gửi thông điệp thoát do lỗi cùng với nguyên nhân chocác process trong link set của mình false X Không kết thúc, nhận được tin nhắn thông báo vào mail box vàxử lý thông tin này như đã được lập trình
Trong đó:
• Trap_exit: là tham số vào cho lời gọi hàm process_flag, nếu là true thì process này là process system, nếu là false thì là process thông thường. • Exit Signal: đây là các tín hiệu thông báo kết thúc từ process khác được
đưa đến process này
o Kill: là exit signal không thể trap được vì vậy khi thông điệp này được gửi đến thì process dù có là system process hay không cũng đều phải kết thúc.
o X: là thông điệp kết thúc do lỗi từ một process khác gửi đến với lý do là X.
o Normal: là tín hiệu kết thúc do process khác gửi đến mà process này kết thúc do hoàn thành công việc của mình chứ không phải do crash.
Từ đây mỗi khi một process được khởi tạo có 3 cách như sau:
1. Tạo một process nhưng không quan tâm đến việc nó có bị crash hay không. Sử dụng lời gọi spawn thông thường:
Pid = spawn(fun() -> ... end)
2. Tạo một process mà cần theo dõi xem nó có bị crash hay không. Ta sẽ sử dụng một lời gọi spawn_link như sau:
Pid = spawn_link(fun() -> ... end)
3. Tạo một process để theo dõi các process khác xem chúng có bị crash hay không. Ta làm như sau:
...
process_flag(trap_exit, true), Pid = spawn_link(fun() -> ... end), ...
loop(...). loop(State) -> receive
{'EXIT', SomePid, Reason} ->
%% do something with the error loop(State1);
... End
Để tạo hiệu quả trong quá trình kiểm soát lỗi các process người ta tạo ra các nhóm các process cùng chức năng tính toán tạo thành một linkset như vậy khi có một process bị crash thì tất cả các process đều kết thúc theo. Như hình dưới
O. Interfacing Technique
Giả sử chúng ta muốn giao diện Erlang có thể kết nối đến một chương trình được viết bằng C hoặc Python hay chạy một kịch bản từ Erlang. Để làm được điều này, chúng ta chạy chương trình từ bên ngoài trong process của hệ điều hành riêng bên ngoài chương trình Erlang lúc runtime, và liên kết process này thông qua kênh giao tiếp hướng byte – byte-oriented communication channel. Sự liên kết của phía Erlang được điều khiển thông qua Erlang port – cổng giao tiếp Erlang. Process mà tạo ra một cổng được gọi là process được kết nối –
connected process cho cổng đó. Quá trình kết nối này có ý nghĩa đặc biệt: tất cả các thông điệp tới chương trình bên ngoài phải được đánh dấu với một PID của process kết nối, và tất cả thông điệp từ chương trình bên ngoài được gửi tới process kết nối này.
Hình 4. Giao tiếp giữa chương trình viết bằng Erlang và ngôn ngữ khác.
Đối với người lập trình, port được đối xử như một Erlang process. Ta có thể gửi thông điệp tới nó, đăng ký nó và nhiều hơn nữa. Nếu chương trình bên ngoại bị treo, thì dấu hiệu exit – kết thúc sẽ được gửi tới process kết nối, và nếu process kết nối bị ngắt, thì chương trình bên ngoài sẽ kết thúc ngay.
Rất nhiều chương trình cho phép code trong các ngôn ngữ khác được kết nối tới ứng dụng có thể được thực hiện được – application executable. Trong Erlang, chúng ta không cho phép điều này vì tính an toàn của nó. Nếu chúng ta kết nối chương trình bến ngoài vào Erlang exe, thì dễ dàng có sự can thiệp của chương trình ngoài gây lỗi cho hệ thống Erlang. Vì vậy, tất cả các ngôn ngữ bên ngoài phải được chạy ngoài hệ thống Erlang trong một hệ thống hoạt động bên ngoài quá trình. Hệ thống Erlang và process bên ngoài kết nối với nhau thông qua luồng byte.
1. Ports:
Cú pháp tạo ra một port như sau:
Port = open_port(PortNam, PortSettings)
Chúng ta sẽ nhận được một port. Thông điệp có thể được gửi tới một port như sau:
Port ! {PidC, {command, Data}}
Gửi dữ liệu (danh sách vào ra) tới cổng này
Port ! {PidC, {connect, Pid1}
Thay đổi PID của process kết nối từ PidC tới Pid1.
Port ! {PidC, close}
Process kết nối có thể nhận được thông điệp từ chương trình ngoài như sau:
receive
{Port, {data, Data}} ->
... Data comes from the external process ...
Dưới đây, chúng ta sẽ tương tác Erlang với một chương tình C hết sức đơn giản.
Chú ý ràng: Ví dụ sau là cố ý đơn giản để làm nổi bật các cơ chế port, giao thức - protocol. Việc mã hóa và giải mã cấu trúc dữ liệu phức tạp là một vấn đề khó, và chúng ta sẽ không đề cập đến ở đây.
2. Tương tác với chương trình C bên ngoài:
example1.c
int twice(int x) { return 2 * x; }
int sum (int x, int y) { return x + y;
}
Chúng ta có thể gọi chương trình này như sau:
X1 = examples1:twice(23), Y1 = examples2:sum(45, 32),
Giao diện của chúng ta cần một chương trình chính mà giả mã dữ liệu được gửi từ chương trình Erlang. Trong ví dụ của chúng ta, trước tiên chúng ta xác định một giao thức giữa cổng và các chương trình C bên ngoài. Chúng ta sẽ sử dụng một cực kỳ đơn giản giao thức và sau đó hiển thị như thế nào để thực hiện điều này trong Erlang và C.