Bagaimana / mengapa bahasa fungsional (khususnya Erlang) berskala dengan baik?

92

Saya telah menyaksikan peningkatan visibilitas bahasa dan fitur pemrograman fungsional untuk sementara waktu. Saya memeriksa mereka dan tidak melihat alasan pengajuan banding.

Kemudian, baru-baru ini saya menghadiri presentasi "Basics of Erlang" Kevin Smith di Codemash .

Saya menikmati presentasi dan belajar bahwa banyak atribut dari pemrograman fungsional membuatnya lebih mudah untuk menghindari masalah threading / konkurensi. Saya memahami kurangnya status dan mutabilitas membuat beberapa utas tidak mungkin mengubah data yang sama, tetapi Kevin mengatakan (jika saya mengerti dengan benar) semua komunikasi terjadi melalui pesan dan pesan diproses secara sinkron (sekali lagi menghindari masalah konkurensi).

Tetapi saya telah membaca bahwa Erlang digunakan dalam aplikasi yang sangat skalabel (alasan utama Ericsson membuatnya sejak awal). Bagaimana bisa efisien menangani ribuan permintaan per detik jika semuanya ditangani sebagai pesan yang diproses secara sinkron? Bukankah itu alasan kita mulai beralih ke pemrosesan asinkron - jadi kita dapat memanfaatkan menjalankan beberapa utas operasi pada saat yang sama dan mencapai skalabilitas? Sepertinya arsitektur ini, meski lebih aman, adalah langkah mundur dalam hal skalabilitas. Apa yang saya lewatkan?

Saya memahami pencipta Erlang sengaja menghindari dukungan threading untuk menghindari masalah konkurensi, tetapi saya pikir multi-threading diperlukan untuk mencapai skalabilitas.

Bagaimana bahasa pemrograman fungsional dapat secara inheren aman untuk thread, namun tetap berskala?

Jim Anderson
sumber
1
[Tidak disebutkan]: VM Erlangs membawa asynchronousness ke level lain. Dengan voodoo magic (asm) ini memungkinkan operasi sinkronisasi seperti socket: read untuk memblokir tanpa menghentikan thread os. Ini memungkinkan Anda untuk menulis kode sinkron ketika bahasa lain memaksa Anda masuk ke dalam sarang panggilan balik asinkron. Jauh lebih mudah untuk menulis aplikasi penskalaan dengan gambaran pikiran dari layanan mikro berulir tunggal VS dengan mengingat gambaran besar setiap kali Anda memasukkan sesuatu ke basis kode.
Van S
@Vans S Menarik.
Jim Anderson

Jawaban:

99

Bahasa fungsional tidak (secara umum) bergantung pada mutasi variabel. Karena itu, kita tidak harus melindungi "status bersama" dari variabel, karena nilainya tetap. Hal ini pada gilirannya menghindari sebagian besar lompatan lingkaran yang harus dilalui bahasa tradisional untuk menerapkan algoritme di seluruh prosesor atau mesin.

Erlang membawanya lebih jauh dari bahasa fungsional tradisional dengan memanggang dalam sistem penyampaian pesan yang memungkinkan segala sesuatu untuk beroperasi pada sistem berbasis peristiwa di mana sepotong kode hanya khawatir tentang menerima pesan dan mengirim pesan, tidak mengkhawatirkan gambaran yang lebih besar.

Artinya adalah bahwa programmer (secara nominal) tidak peduli bahwa pesan akan ditangani pada prosesor atau mesin lain: cukup mengirim pesan saja sudah cukup untuk dilanjutkan. Jika peduli dengan tanggapan, itu akan menunggu sebagai pesan lain .

Hasil akhirnya adalah bahwa setiap cuplikan tidak bergantung pada setiap cuplikan lainnya. Tidak ada kode bersama, tidak ada keadaan bersama dan semua interaksi yang berasal dari sistem pesan yang dapat didistribusikan di antara banyak perangkat keras (atau tidak).

Bandingkan ini dengan sistem tradisional: kita harus menempatkan mutex dan semaphore di sekitar variabel "dilindungi" dan eksekusi kode. Kami memiliki pengikatan yang ketat dalam panggilan fungsi melalui tumpukan (menunggu kembalinya terjadi). Semua ini menciptakan kemacetan yang bukan merupakan masalah dalam sistem tidak ada bersama seperti Erlang.

EDIT: Saya juga harus menunjukkan bahwa Erlang tidak sinkron. Anda mengirim pesan Anda dan mungkin / suatu hari pesan lain datang kembali. Atau tidak.

Poin Spencer tentang eksekusi out of order juga penting dan dijawab dengan baik.

Godeke
sumber
Saya memahami ini, tetapi tidak melihat bagaimana model pesan itu efisien. Saya akan menebak sebaliknya. Ini benar-benar membuka mata saya. Tidak heran jika bahasa pemrograman fungsional mendapatkan begitu banyak perhatian.
Jim Anderson
3
Anda mendapatkan banyak potensi konkurensi dalam sistem tidak ada yang dibagikan. Penerapan yang buruk (pesan tinggi lewat overhead, misalnya) dapat merusak ini, tetapi Erlang tampaknya melakukannya dengan benar dan menjaga semuanya tetap ringan.
Godeke
Penting untuk dicatat bahwa meskipun Erlang memiliki semantik pesan yang lewat, ia memiliki implementasi memori bersama, oleh karena itu, ia memiliki semantik yang dijelaskan tetapi tidak menyalin barang di semua tempat jika tidak perlu.
Aaron Maenpaa
1
@Godeke: "Erlang (seperti kebanyakan bahasa fungsional) menyimpan satu contoh data apa pun bila memungkinkan". AFAIK, Erlang sebenarnya menyalin dalam semua yang melewati antara proses ringannya karena kurangnya GC bersamaan.
JD
1
@JonHarrop hampir benar: ketika suatu proses mengirim pesan ke proses lain, pesan tersebut disalin; kecuali untuk biner besar, yang diteruskan dengan referensi. Lihat misalnya jlouisramblings.blogspot.hu/2013/10/embrace-copying.html untuk mengetahui mengapa ini adalah hal yang baik.
hcs42
74

Sistem antrian pesan itu keren karena secara efektif menghasilkan efek "api-dan-tunggu-hasil" yang merupakan bagian sinkron yang sedang Anda baca. Apa yang membuat ini luar biasa adalah itu berarti garis tidak perlu dieksekusi secara berurutan. Perhatikan kode berikut:

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

Pertimbangkan sejenak bahwa methodWithALotOfDiskProcessing () membutuhkan waktu sekitar 2 detik untuk menyelesaikannya dan methodWithALotOfNetworkProcessing () membutuhkan waktu sekitar 1 detik untuk menyelesaikannya. Dalam bahasa prosedural, kode ini akan memakan waktu sekitar 3 detik untuk dijalankan karena garis akan dieksekusi secara berurutan. Kami membuang-buang waktu menunggu satu metode untuk diselesaikan yang dapat berjalan bersamaan dengan yang lain tanpa bersaing untuk satu sumber daya. Dalam bahasa fungsional, baris kode tidak menentukan kapan prosesor akan mencobanya. Bahasa fungsional akan mencoba sesuatu seperti berikut:

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.

Betapa kerennya itu? Dengan melanjutkan kode dan hanya menunggu jika perlu, kami telah mengurangi waktu tunggu menjadi dua detik secara otomatis! : D Jadi ya, meskipun kodenya sinkron, ia cenderung memiliki arti yang berbeda dari pada bahasa prosedural.

EDIT:

Setelah Anda memahami konsep ini dalam hubungannya dengan posting Godeke, mudah untuk membayangkan betapa sederhananya memanfaatkan banyak prosesor, server farm, penyimpanan data yang berlebihan, dan entah apa lagi.

Spencer Ruport
sumber
Keren! Saya benar-benar salah paham bagaimana pesan ditangani. Terima kasih, postingan Anda membantu.
Jim Anderson
"Bahasa fungsional akan mencoba sesuatu seperti berikut" - Saya tidak yakin tentang bahasa fungsional lainnya, tetapi di Erlang contoh akan bekerja persis seperti dalam kasus bahasa prosedural. Anda dapat melakukan dua tugas tersebut secara paralel dengan proses pemijahan, membiarkan mereka menjalankan dua tugas secara asinkron, dan mendapatkan hasilnya di akhir, tetapi ini tidak seperti "sementara kode sinkron itu cenderung memiliki arti yang berbeda dengan bahasa prosedural. " Lihat juga jawaban Chris.
hcs42
16

Sepertinya Anda mencampurkan sinkron dengan sekuensial .

Tubuh suatu fungsi dalam erlang sedang diproses secara berurutan. Jadi apa yang Spencer katakan tentang "efek automagical" ini tidak berlaku untuk erlang. Anda bisa memodelkan perilaku ini dengan erlang.

Misalnya Anda bisa menelurkan proses yang menghitung jumlah kata dalam satu baris. Karena kami memiliki beberapa baris, kami menelurkan satu proses seperti itu untuk setiap baris dan menerima jawaban untuk menghitung jumlah darinya.

Dengan begitu, kami menelurkan proses yang melakukan penghitungan "berat" (menggunakan inti tambahan jika tersedia) dan kemudian kami mengumpulkan hasilnya.

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

Dan seperti inilah tampilannya, ketika kita menjalankan ini di 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> 
Chris Czura
sumber
13

Hal utama yang memungkinkan Erlang melakukan penskalaan terkait dengan konkurensi.

Sistem operasi menyediakan konkurensi dengan dua mekanisme:

  • proses sistem operasi
  • utas sistem operasi

Proses tidak berbagi status - satu proses tidak dapat merusak proses lainnya dengan sengaja.

Status berbagi utas - satu utas dapat merusak utas lain karena disain - itu masalah Anda.

Dengan Erlang - satu proses sistem operasi digunakan oleh mesin virtual dan VM menyediakan konkurensi ke program Erlang tidak dengan menggunakan utas sistem operasi tetapi dengan menyediakan proses Erlang - yaitu Erlang mengimplementasikan pengganda waktunya sendiri.

Proses Erlang ini berbicara satu sama lain dengan mengirim pesan (ditangani oleh VM Erlang bukan sistem operasi). Proses Erlang menangani satu sama lain menggunakan ID proses (PID) yang memiliki alamat tiga bagian <<N3.N2.N1>>:

  • proses tanpa N1
  • VM N2 aktif
  • mesin fisik N3

Dua proses pada VM yang sama, pada VM yang berbeda pada mesin yang sama atau dua mesin berkomunikasi dengan cara yang sama - oleh karena itu penskalaan Anda tidak bergantung pada jumlah mesin fisik tempat Anda menerapkan aplikasi (pada perkiraan pertama).

Erlang hanya threadsafe dalam arti sepele - ia tidak memiliki benang. (Bahasanya, VM SMP / multi-core menggunakan satu thread sistem operasi per inti).

Gordon Guthrie
sumber
7

Anda mungkin salah paham tentang cara kerja Erlang. Runtime Erlang meminimalkan pengalihan konteks pada CPU, tetapi jika ada beberapa CPU yang tersedia, semuanya digunakan untuk memproses pesan. Anda tidak memiliki "untaian" seperti yang Anda lakukan dalam bahasa lain, tetapi Anda dapat memiliki banyak pesan yang diproses secara bersamaan.

Kristopher Johnson
sumber
4

Pesan erlang murni asinkron, jika Anda ingin balasan sinkron ke pesan Anda, Anda perlu mengkodekannya secara eksplisit. Apa yang mungkin dikatakan adalah bahwa pesan dalam kotak pesan proses diproses secara berurutan. Setiap pesan yang dikirim ke suatu proses akan ditempatkan di kotak pesan proses itu, dan proses tersebut dapat memilih satu pesan dari kotak itu, memprosesnya, dan kemudian melanjutkan ke pesan berikutnya, dalam urutan yang dianggap sesuai. Ini adalah tindakan yang sangat berurutan dan blok penerima melakukan hal itu.

Sepertinya Anda telah mencampurkan sinkron dan berurutan seperti yang disebutkan chris.

Jebu
sumber
-2

Dalam bahasa fungsional murni, urutan evaluasi tidak menjadi masalah - dalam aplikasi fungsi fn (arg1, .. argn), n argumen dapat dievaluasi secara paralel. Itu menjamin tingkat paralelisme (otomatis) yang tinggi.

Erlang menggunakan model proses di mana proses dapat berjalan di mesin virtual yang sama, atau pada prosesor yang berbeda - tidak ada cara untuk membedakannya. Itu hanya mungkin karena pesan disalin di antara proses, tidak ada status bersama (bisa berubah). Paralelisme multi-prosesor berjalan lebih jauh daripada multi-threading, karena utas bergantung pada memori bersama, ini hanya dapat ada 8 utas yang berjalan secara paralel pada CPU 8-inti, sementara multi-pemrosesan dapat menskalakan hingga ribuan proses paralel.

mfx
sumber