Làm thế nào / tại sao các ngôn ngữ chức năng (cụ thể là Erlang) mở rộng quy mô tốt?


92

Tôi đã theo dõi khả năng hiển thị ngày càng tăng của các ngôn ngữ lập trình chức năng và các tính năng trong một thời gian. Tôi đã xem xét chúng và không thấy lý do kháng cáo.

Sau đó, gần đây tôi đã tham dự buổi thuyết trình "Khái niệm cơ bản về Erlang" của Kevin Smith tại Codemash .

Tôi rất thích bài thuyết trình và biết rằng rất nhiều thuộc tính của lập trình hàm giúp tránh các vấn đề về luồng / đồng thời dễ dàng hơn nhiều. Tôi hiểu việc thiếu trạng thái và khả năng thay đổi khiến nhiều luồng không thể thay đổi cùng một dữ liệu, nhưng Kevin cho biết (nếu tôi hiểu đúng) tất cả giao tiếp diễn ra thông qua các tin nhắn và các tin nhắn được xử lý đồng bộ (tránh các vấn đề đồng thời).

Nhưng tôi đã đọc rằng Erlang được sử dụng trong các ứng dụng có khả năng mở rộng cao (toàn bộ lý do Ericsson tạo ra nó ngay từ đầu). Làm thế nào nó có thể xử lý hiệu quả hàng nghìn yêu cầu mỗi giây nếu mọi thứ được xử lý như một thông báo được xử lý đồng bộ? Đó không phải là lý do tại sao chúng tôi bắt đầu chuyển sang xử lý không đồng bộ - để chúng tôi có thể tận dụng lợi thế của việc chạy nhiều luồng hoạt động cùng một lúc và đạt được khả năng mở rộng? Có vẻ như kiến ​​trúc này, mặc dù an toàn hơn, nhưng lại là một bước lùi về khả năng mở rộng. Tôi đang thiếu gì?

Tôi hiểu những người tạo ra Erlang đã cố ý tránh hỗ trợ phân luồng để tránh các vấn đề đồng thời, nhưng tôi nghĩ đa luồng là cần thiết để đạt được khả năng mở rộng.

Làm thế nào để ngôn ngữ lập trình chức năng có thể an toàn theo luồng, nhưng vẫn mở rộng quy mô?


1
[Không được đề cập]: Máy ảo của Erlangs đưa tính không đồng bộ lên một cấp độ khác. Bằng phép thuật voodoo (asm), nó cho phép các hoạt động đồng bộ hóa như socket: đọc để chặn mà không dừng một chuỗi hệ điều hành. Điều này cho phép bạn viết mã đồng bộ khi các ngôn ngữ khác buộc bạn vào các tổ gọi lại không đồng bộ. Sẽ dễ dàng hơn nhiều khi viết một ứng dụng mở rộng quy mô với hình dung về các dịch vụ vi mô đơn luồng VS lưu giữ bức tranh toàn cảnh mỗi khi bạn ghim thứ gì đó vào cơ sở mã.
Vans S

@Vans S Thật thú vị.
Jim Anderson

Câu trả lời:


99

Một ngôn ngữ chức năng (nói chung) không dựa vào việc thay đổi một biến. Do đó, chúng ta không phải bảo vệ "trạng thái được chia sẻ" của một biến, bởi vì giá trị là cố định. Điều này sẽ tránh được phần lớn các bước nhảy vòng mà các ngôn ngữ truyền thống phải trải qua để thực hiện một thuật toán trên các bộ xử lý hoặc máy móc.

Erlang đưa nó đi xa hơn các ngôn ngữ chức năng truyền thống bằng cách nướng trong một hệ thống truyền tin nhắn cho phép mọi thứ hoạt động trên một hệ thống dựa trên sự kiện, nơi một đoạn mã chỉ lo lắng về việc nhận tin nhắn và gửi tin nhắn, không phải lo lắng về một bức tranh lớn hơn.

Điều này có nghĩa là lập trình viên (trên danh nghĩa) không quan tâm đến việc thông điệp sẽ được xử lý trên một bộ xử lý hoặc máy khác: chỉ cần gửi thông điệp là đủ tốt để nó tiếp tục. Nếu nó quan tâm đến một phản hồi, nó sẽ đợi nó như một tin nhắn khác .

Kết quả cuối cùng của việc này là mỗi đoạn mã độc lập với mọi đoạn mã khác. Không có mã được chia sẻ, không có trạng thái được chia sẻ và tất cả các tương tác đến từ một hệ thống thông báo có thể được phân phối giữa nhiều phần cứng (hoặc không).

Ngược lại điều này với hệ thống truyền thống: chúng ta phải đặt mutexes và semaphores xung quanh các biến "được bảo vệ" và thực thi mã. Chúng ta có ràng buộc chặt chẽ trong một lệnh gọi hàm thông qua ngăn xếp (đang chờ sự trả về xảy ra). Tất cả những điều này tạo ra những nút thắt cổ chai ít gặp vấn đề hơn trong một hệ thống không được chia sẻ như Erlang.

CHỈNH SỬA: Tôi cũng nên chỉ ra rằng Erlang là không đồng bộ. Bạn gửi tin nhắn của mình và có thể / một ngày nào đó một tin nhắn khác sẽ gửi lại. Hay không.

Quan điểm của Spencer về việc thực hiện đơn đặt hàng cũng rất quan trọng và được giải đáp rõ ràng.


Tôi hiểu điều này, nhưng không thấy mô hình thông báo hiệu quả như thế nào. Tôi sẽ đoán ngược lại. Đây là một thứ mở mang tầm mắt cho tôi. Không có gì ngạc nhiên khi các ngôn ngữ lập trình chức năng đang được chú ý nhiều như vậy.
Jim Anderson

3
Bạn có được rất nhiều tiềm năng đồng thời trong một hệ thống không chia sẻ. Một cách triển khai không tốt (ví dụ: thông báo vượt quá cao) có thể gây ra lỗi này, nhưng Erlang dường như đã làm đúng và giữ cho mọi thứ nhẹ nhàng.
Godeke

Điều quan trọng cần lưu ý là trong khi Erlang có ngữ nghĩa truyền thông điệp, nó có triển khai bộ nhớ chia sẻ, do đó, nó có ngữ nghĩa được mô tả nhưng nó không sao chép mọi thứ ở khắp nơi nếu không cần thiết.
Aaron Maenpaa

1
@Godeke: "Erlang (giống như hầu hết các ngôn ngữ chức năng) giữ một phiên bản dữ liệu duy nhất khi có thể". AFAIK, Erlang thực sự sao chép sâu mọi thứ được chuyển qua giữa các quy trình nhẹ của nó do thiếu GC đồng thời.
JD

1
@JonHarrop gần như đúng: khi một quy trình gửi một thông báo đến một quy trình khác, thông báo đó sẽ được sao chép; ngoại trừ các mã nhị phân lớn, được chuyển qua tham chiếu. Xem ví dụ: jlouisramblings.blogspot.hu/2013/10/embrace-copying.html để biết lý do tại sao đây là một điều tốt.
hcs42

74

Hệ thống hàng đợi tin nhắn rất tuyệt vì nó tạo ra hiệu ứng "cháy và chờ kết quả" một cách hiệu quả, là phần đồng bộ mà bạn đang đọc. Điều làm cho điều này trở nên vô cùng tuyệt vời là nó có nghĩa là các dòng không cần phải được thực hiện tuần tự. Hãy xem xét đoạn mã sau:

r = methodWithALotOfDiskProcessing();
x = r + 1;
y = methodWithALotOfNetworkProcessing();
w = x * y

Hãy xem xét một chút rằng methodWithALotOfDiskProcessing () mất khoảng 2 giây để hoàn thành và methodWithALotOfNetworkProcessing () mất khoảng 1 giây để hoàn thành. Trong một ngôn ngữ thủ tục, mã này sẽ mất khoảng 3 giây để chạy vì các dòng sẽ được thực hiện tuần tự. Chúng tôi đang lãng phí thời gian chờ đợi một phương pháp hoàn thành có thể chạy đồng thời với phương pháp kia mà không phải cạnh tranh cho một tài nguyên nào. Trong ngôn ngữ chức năng, các dòng mã không chỉ định khi nào bộ xử lý sẽ xử lý chúng. Một ngôn ngữ chức năng sẽ thử một cái gì đó như sau:

Execute line 1 ... wait.
Execute line 2 ... wait for r value.
Execute line 3 ... wait.
Execute line 4 ... wait for x and y value.
Line 3 returned ... y value set, message line 4.
Line 1 returned ... r value set, message line 2.
Line 2 returned ... x value set, message line 4.
Line 4 returned ... done.

Làm thế nào là thú vị? Bằng cách tiếp tục với mã và chỉ đợi khi cần thiết, chúng tôi đã tự động giảm thời gian chờ xuống còn hai giây! : D Vì vậy, trong khi mã là đồng bộ, nó có xu hướng có ý nghĩa khác với các ngôn ngữ thủ tục.

BIÊN TẬP:

Một khi bạn nắm bắt được khái niệm này cùng với bài đăng của Godeke, bạn sẽ dễ dàng tưởng tượng việc tận dụng nhiều bộ xử lý, máy chủ, kho lưu trữ dữ liệu dư thừa và những thứ khác sẽ trở nên đơn giản như thế nào .


Mát mẻ! Tôi hoàn toàn hiểu sai về cách xử lý tin nhắn. Cảm ơn, bài viết của bạn giúp ích.
Jim Anderson

"Một ngôn ngữ chức năng sẽ thử một cái gì đó như sau" - Tôi không chắc về các ngôn ngữ chức năng khác, nhưng trong Erlang, ví dụ sẽ hoạt động chính xác như trong trường hợp ngôn ngữ thủ tục. Bạn có thể thực hiện song song hai tác vụ đó bằng cách sinh ra các tiến trình, cho phép chúng thực hiện hai tác vụ không đồng bộ và thu được kết quả ở cuối, nhưng không giống như "trong khi mã đồng bộ, nó có xu hướng có ý nghĩa khác với ngôn ngữ thủ tục. " Xem thêm câu trả lời của Chris.
hcs42,

16

Có khả năng bạn đang trộn đồng bộ với tuần tự .

Phần thân của một hàm trong erlang đang được xử lý tuần tự. Vì vậy, những gì Spencer nói về "hiệu ứng tự động" này không đúng với erlang. Tuy nhiên, bạn có thể lập mô hình hành vi này với erlang.

Ví dụ, bạn có thể tạo ra một quy trình tính toán số lượng từ trong một dòng. Vì chúng tôi có một số dòng, chúng tôi tạo ra một quy trình như vậy cho mỗi dòng và nhận câu trả lời để tính tổng từ đó.

Bằng cách đó, chúng tôi tạo ra các quy trình thực hiện các phép tính "nặng" (sử dụng các lõi bổ sung nếu có) và sau đó chúng tôi thu thập kết quả.

-module(countwords).
-export([count_words_in_lines/1]).

count_words_in_lines(Lines) ->
    % For each line in lines run spawn_summarizer with the process id (pid)
    % and a line to work on as arguments.
    % This is a list comprehension and spawn_summarizer will return the pid
    % of the process that was created. So the variable Pids will hold a list
    % of process ids.
    Pids = [spawn_summarizer(self(), Line) || Line <- Lines], 
    % For each pid receive the answer. This will happen in the same order in
    % which the processes were created, because we saved [pid1, pid2, ...] in
    % the variable Pids and now we consume this list.
    Results = [receive_result(Pid) || Pid <- Pids],
    % Sum up the results.
    WordCount = lists:sum(Results),
    io:format("We've got ~p words, Sir!~n", [WordCount]).

spawn_summarizer(S, Line) ->
    % Create a anonymous function and save it in the variable F.
    F = fun() ->
        % Split line into words.
        ListOfWords = string:tokens(Line, " "),
        Length = length(ListOfWords),
        io:format("process ~p calculated ~p words~n", [self(), Length]),
        % Send a tuple containing our pid and Length to S.
        S ! {self(), Length}
    end,
    % There is no return in erlang, instead the last value in a function is
    % returned implicitly.
    % Spawn the anonymous function and return the pid of the new process.
    spawn(F).

% The Variable Pid gets bound in the function head.
% In erlang, you can only assign to a variable once.
receive_result(Pid) ->
    receive
        % Pattern-matching: the block behind "->" will execute only if we receive
        % a tuple that matches the one below. The variable Pid is already bound,
        % so we are waiting here for the answer of a specific process.
        % N is unbound so we accept any value.
        {Pid, N} ->
            io:format("Received \"~p\" from process ~p~n", [N, Pid]),
            N
    end.

Và đây là những gì nó trông như thế này, khi chúng ta chạy nó trong shell:

Eshell V5.6.5  (abort with ^G)
1> Lines = ["This is a string of text", "and this is another", "and yet another", "it's getting boring now"].
["This is a string of text","and this is another",
 "and yet another","it's getting boring now"]
2> c(countwords).
{ok,countwords}
3> countwords:count_words_in_lines(Lines).
process <0.39.0> calculated 6 words
process <0.40.0> calculated 4 words
process <0.41.0> calculated 3 words
process <0.42.0> calculated 4 words
Received "6" from process <0.39.0>
Received "4" from process <0.40.0>
Received "3" from process <0.41.0>
Received "4" from process <0.42.0>
We've got 17 words, Sir!
ok
4> 

13

Điều quan trọng cho phép Erlang mở rộng quy mô là liên quan đến đồng thời.

Một hệ điều hành cung cấp sự đồng thời theo hai cơ chế:

  • quy trình hệ điều hành
  • chủ đề hệ điều hành

Các quy trình không chia sẻ trạng thái - một quy trình không thể làm hỏng quy trình khác theo thiết kế.

Trạng thái chia sẻ luồng - một luồng có thể làm hỏng một luồng khác theo thiết kế - đó là vấn đề của bạn.

Với Erlang - một quy trình của hệ điều hành được sử dụng bởi máy ảo và VM cung cấp sự đồng thời cho chương trình Erlang không phải bằng cách sử dụng các luồng hệ điều hành mà bằng cách cung cấp các quy trình Erlang - tức là Erlang thực hiện bộ đếm thời gian của riêng nó.

Các tiến trình Erlang này nói chuyện với nhau bằng cách gửi tin nhắn (do Erlang VM xử lý không phải hệ điều hành). Các quy trình Erlang giải quyết lẫn nhau bằng cách sử dụng ID quy trình (PID) có địa chỉ gồm ba phần <<N3.N2.N1>>:

  • xử lý không có N1 trên
  • VM N2 trên
  • máy vật lý N3

Hai quy trình trên cùng một máy ảo, trên các máy ảo khác nhau trên cùng một máy hoặc hai máy giao tiếp theo cùng một cách - do đó tỷ lệ của bạn không phụ thuộc vào số lượng máy vật lý mà bạn triển khai ứng dụng của mình (trong ước tính đầu tiên).

Erlang chỉ là threadsafe theo nghĩa tầm thường - nó không có thread. (Ngôn ngữ có nghĩa là SMP / VM đa lõi sử dụng một luồng hệ điều hành cho mỗi lõi).


7

Bạn có thể hiểu sai về cách hoạt động của Erlang. Thời gian chạy Erlang giảm thiểu chuyển đổi ngữ cảnh trên CPU, nhưng nếu có nhiều CPU thì tất cả đều được sử dụng để xử lý thông báo. Bạn không có "chuỗi" theo nghĩa bạn làm bằng các ngôn ngữ khác, nhưng bạn có thể có nhiều thư được xử lý đồng thời.


4

Tin nhắn erlang hoàn toàn là không đồng bộ, nếu bạn muốn trả lời đồng bộ cho tin nhắn của mình, bạn cần phải viết mã rõ ràng cho điều đó. Điều có thể nói là các thông báo trong hộp thông báo quy trình được xử lý tuần tự. Bất kỳ thông báo nào được gửi đến một quy trình sẽ nằm trong hộp thông báo quy trình đó và quy trình sẽ chọn một thông báo từ hộp đó xử lý và sau đó chuyển sang thư tiếp theo, theo thứ tự mà nó thấy phù hợp. Đây là một hành động rất tuần tự và khối nhận thực hiện chính xác điều đó.

Có vẻ như bạn đã trộn đồng bộ và tuần tự như chris đã đề cập.



-2

Trong một ngôn ngữ chức năng thuần túy, thứ tự đánh giá không quan trọng - trong một ứng dụng hàm fn (arg1, .. argn), n đối số có thể được đánh giá song song. Điều đó đảm bảo mức độ song song (tự động) cao.

Erlang sử dụng một modell quy trình trong đó một quy trình có thể chạy trong cùng một máy ảo hoặc trên một bộ xử lý khác - không có cách nào để nói. Điều đó chỉ có thể thực hiện được vì thông báo được sao chép giữa các tiến trình, không có trạng thái chia sẻ (có thể thay đổi). Phân chia đa xử lý đi xa hơn nhiều so với đa luồng, vì các luồng phụ thuộc vào bộ nhớ được chia sẻ, điều này chỉ có thể có 8 luồng chạy song song trên một CPU 8 nhân, trong khi đa xử lý có thể mở rộng đến hàng nghìn quy trình song song.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.